summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
committerMarie <Marie@kaifa.ch>2023-12-23 02:09:23 +0100
commit5db583a3eb61d50de14d875ebf7ecef20490e313 (patch)
tree783dd43d2ac660c32e745a4485d499e9ddc43324 /packages/frontend/src/components
parentadd: Custom MOTDs (diff)
parentUpdate CHANGELOG.md (diff)
downloadsharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.gz
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.tar.bz2
sharkey-5db583a3eb61d50de14d875ebf7ecef20490e313.zip
merge: upstream
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue7
-rw-r--r--packages/frontend/src/components/MkAchievements.vue10
-rw-r--r--packages/frontend/src/components/MkAnalogClock.vue64
-rw-r--r--packages/frontend/src/components/MkAnimBg.vue32
-rw-r--r--packages/frontend/src/components/MkAsUi.vue8
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue111
-rw-r--r--packages/frontend/src/components/MkButton.vue14
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue98
-rw-r--r--packages/frontend/src/components/MkChart.vue4
-rw-r--r--packages/frontend/src/components/MkChartLegend.vue19
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue14
-rw-r--r--packages/frontend/src/components/MkCode.core.vue4
-rw-r--r--packages/frontend/src/components/MkCode.vue65
-rw-r--r--packages/frontend/src/components/MkCodeEditor.vue92
-rw-r--r--packages/frontend/src/components/MkColorInput.vue3
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue16
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue16
-rw-r--r--packages/frontend/src/components/MkCwButton.vue25
-rw-r--r--packages/frontend/src/components/MkDialog.vue19
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue9
-rw-r--r--packages/frontend/src/components/MkDrive.vue4
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue47
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue83
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue13
-rw-r--r--packages/frontend/src/components/MkFeaturedPhotos.vue2
-rw-r--r--packages/frontend/src/components/MkFileCaptionEditWindow.vue10
-rw-r--r--packages/frontend/src/components/MkFolder.vue20
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue26
-rw-r--r--packages/frontend/src/components/MkForgotPassword.vue18
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue4
-rw-r--r--packages/frontend/src/components/MkGoogle.vue2
-rw-r--r--packages/frontend/src/components/MkHeatmap.vue18
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue45
-rw-r--r--packages/frontend/src/components/MkInput.vue15
-rw-r--r--packages/frontend/src/components/MkInstanceCardMini.vue9
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue16
-rw-r--r--packages/frontend/src/components/MkInstanceTicker.vue4
-rw-r--r--packages/frontend/src/components/MkInviteCode.vue2
-rw-r--r--packages/frontend/src/components/MkLaunchPad.vue8
-rw-r--r--packages/frontend/src/components/MkLink.vue10
-rw-r--r--packages/frontend/src/components/MkMediaBanner.vue4
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue22
-rw-r--r--packages/frontend/src/components/MkMediaList.vue81
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue13
-rw-r--r--packages/frontend/src/components/MkMenu.vue98
-rw-r--r--packages/frontend/src/components/MkMiniChart.vue20
-rw-r--r--packages/frontend/src/components/MkModPlayer.vue10
-rw-r--r--packages/frontend/src/components/MkModal.vue92
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue24
-rw-r--r--packages/frontend/src/components/MkNote.vue195
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue145
-rw-r--r--packages/frontend/src/components/MkNotePreview.vue28
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue9
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue43
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotification.vue8
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue6
-rw-r--r--packages/frontend/src/components/MkNotifications.vue11
-rw-r--r--packages/frontend/src/components/MkOmit.vue14
-rw-r--r--packages/frontend/src/components/MkPagePreview.vue2
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue40
-rw-r--r--packages/frontend/src/components/MkPagination.vue63
-rw-r--r--packages/frontend/src/components/MkPasswordDialog.vue18
-rw-r--r--packages/frontend/src/components/MkPlusOneEffect.vue6
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue10
-rw-r--r--packages/frontend/src/components/MkPostForm.vue407
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue9
-rw-r--r--packages/frontend/src/components/MkPullToRefresh.vue79
-rw-r--r--packages/frontend/src/components/MkPushNotificationAllowButton.vue41
-rw-r--r--packages/frontend/src/components/MkRadio.vue4
-rw-r--r--packages/frontend/src/components/MkReactionEffect.vue6
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue17
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.vue26
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue16
-rw-r--r--packages/frontend/src/components/MkSignin.vue90
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue8
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue152
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue4
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue13
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue25
-rw-r--r--packages/frontend/src/components/MkSwitch.vue5
-rw-r--r--packages/frontend/src/components/MkTagCloud.vue20
-rw-r--r--packages/frontend/src/components/MkTextarea.vue41
-rw-r--r--packages/frontend/src/components/MkTimeline.vue10
-rw-r--r--packages/frontend/src/components/MkToast.vue6
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue26
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Timeline.vue2
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue62
-rw-r--r--packages/frontend/src/components/MkUrlPreviewPopup.vue10
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue32
-rw-r--r--packages/frontend/src/components/MkUserCardMini.vue6
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue6
-rw-r--r--packages/frontend/src/components/MkUserOnlineIndicator.vue4
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue24
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue40
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue10
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue3
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.vue1
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue10
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue10
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue29
-rw-r--r--packages/frontend/src/components/MkWindow.vue96
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue15
-rw-r--r--packages/frontend/src/components/SkApprovalUser.vue9
-rw-r--r--packages/frontend/src/components/SkInstanceTicker.vue4
-rw-r--r--packages/frontend/src/components/SkNote.vue193
-rw-r--r--packages/frontend/src/components/SkNoteDetailed.vue145
-rw-r--r--packages/frontend/src/components/SkNoteSimple.vue9
-rw-r--r--packages/frontend/src/components/SkNoteSub.vue43
-rw-r--r--packages/frontend/src/components/SkOldNoteWindow.vue33
-rw-r--r--packages/frontend/src/components/form/section.vue9
-rw-r--r--packages/frontend/src/components/form/suspense.vue1
-rw-r--r--packages/frontend/src/components/global/MkA.vue10
-rw-r--r--packages/frontend/src/components/global/MkAd.stories.impl.ts3
-rw-r--r--packages/frontend/src/components/global/MkAd.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue76
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue6
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue2
-rw-r--r--packages/frontend/src/components/global/MkLazy.vue53
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts40
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue36
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue62
-rw-r--r--packages/frontend/src/components/global/MkTime.vue61
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/global/MkUserName.stories.impl.ts1
-rw-r--r--packages/frontend/src/components/global/RouterView.vue14
-rw-r--r--packages/frontend/src/components/index.ts3
-rw-r--r--packages/frontend/src/components/page/page.text.vue1
-rw-r--r--packages/frontend/src/components/page/page.vue1
131 files changed, 2328 insertions, 1814 deletions
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 14247f4bf5..611c8a1782 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="report.comment"/>
</div>
<hr/>
- <div>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></div>
+ <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
<div v-if="report.assignee">
{{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/>
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
@@ -56,11 +57,11 @@ const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
-let forward = $ref(props.report.forwarded);
+const forward = ref(props.report.forwarded);
function resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
- forward: forward,
+ forward: forward.value,
reportId: props.report.id,
}).then(() => {
emit('resolved', props.report.id);
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index aac7f508a1..40f9ad4057 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { onMounted } from 'vue';
+import { onMounted, ref, computed } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
@@ -67,15 +67,15 @@ const props = withDefaults(defineProps<{
withDescription: true,
});
-let achievements = $ref();
-const lockedAchievements = $computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements ?? []).some(a => a.name === x)));
+const achievements = ref();
+const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() {
os.api('users/achievements', { userId: props.user.id }).then(res => {
- achievements = [];
+ achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) {
const a = res.find(x => x.name === t);
- if (a) achievements.push(a);
+ if (a) achievements.value.push(a);
}
//achievements = res.sort((a, b) => b.unlockedAt - a.unlockedAt);
});
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index cd2c4d8264..0e252f7b1d 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -138,45 +138,45 @@ const texts = computed(() => {
});
let enabled = true;
-let majorGraduationColor = $ref<string>();
+const majorGraduationColor = ref<string>();
//let minorGraduationColor = $ref<string>();
-let sHandColor = $ref<string>();
-let mHandColor = $ref<string>();
-let hHandColor = $ref<string>();
-let nowColor = $ref<string>();
-let h = $ref<number>(0);
-let m = $ref<number>(0);
-let s = $ref<number>(0);
-let hAngle = $ref<number>(0);
-let mAngle = $ref<number>(0);
-let sAngle = $ref<number>(0);
-let disableSAnimate = $ref(false);
+const sHandColor = ref<string>();
+const mHandColor = ref<string>();
+const hHandColor = ref<string>();
+const nowColor = ref<string>();
+const h = ref<number>(0);
+const m = ref<number>(0);
+const s = ref<number>(0);
+const hAngle = ref<number>(0);
+const mAngle = ref<number>(0);
+const sAngle = ref<number>(0);
+const disableSAnimate = ref(false);
let sOneRound = false;
const sLine = ref<SVGPathElement>();
function tick() {
const now = props.now();
now.setMinutes(now.getMinutes() + now.getTimezoneOffset() + props.offset);
- const previousS = s;
- const previousM = m;
- const previousH = h;
- s = now.getSeconds();
- m = now.getMinutes();
- h = now.getHours();
- if (previousS === s && previousM === m && previousH === h) {
+ const previousS = s.value;
+ const previousM = m.value;
+ const previousH = h.value;
+ s.value = now.getSeconds();
+ m.value = now.getMinutes();
+ h.value = now.getHours();
+ if (previousS === s.value && previousM === m.value && previousH === h.value) {
return;
}
- hAngle = Math.PI * (h % (props.twentyfour ? 24 : 12) + (m + s / 60) / 60) / (props.twentyfour ? 12 : 6);
- mAngle = Math.PI * (m + s / 60) / 30;
+ hAngle.value = Math.PI * (h.value % (props.twentyfour ? 24 : 12) + (m.value + s.value / 60) / 60) / (props.twentyfour ? 12 : 6);
+ mAngle.value = Math.PI * (m.value + s.value / 60) / 30;
if (sOneRound && sLine.value) { // 秒針が一周した際のアニメーションをよしなに処理する(これが無いと秒が59->0になったときに期待したアニメーションにならない)
- sAngle = Math.PI * 60 / 30;
+ sAngle.value = Math.PI * 60 / 30;
defaultIdlingRenderScheduler.delete(tick);
sLine.value.addEventListener('transitionend', () => {
- disableSAnimate = true;
+ disableSAnimate.value = true;
requestAnimationFrame(() => {
- sAngle = 0;
+ sAngle.value = 0;
requestAnimationFrame(() => {
- disableSAnimate = false;
+ disableSAnimate.value = false;
if (enabled) {
defaultIdlingRenderScheduler.add(tick);
}
@@ -184,9 +184,9 @@ function tick() {
});
}, { once: true });
} else {
- sAngle = Math.PI * s / 30;
+ sAngle.value = Math.PI * s.value / 30;
}
- sOneRound = s === 59;
+ sOneRound = s.value === 59;
}
tick();
@@ -195,12 +195,12 @@ function calcColors() {
const computedStyle = getComputedStyle(document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
- majorGraduationColor = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
+ majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
- sHandColor = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
- mHandColor = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
- hHandColor = accent;
- nowColor = accent;
+ sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
+ mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
+ hHandColor.value = accent;
+ nowColor.value = accent;
}
calcColors();
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
index 70d101a9d3..284ee8f3f8 100644
--- a/packages/frontend/src/components/MkAnimBg.vue
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -21,8 +21,9 @@ const props = withDefaults(defineProps<{
focus: 1.0,
});
-function loadShader(gl, type, source) {
+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);
@@ -38,11 +39,13 @@ function loadShader(gl, type, source) {
return shader;
}
-function initShaderProgram(gl, vsSource, fsSource) {
+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 (shaderProgram == null || vertexShader == null || fragmentShader == null) return null;
+
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
@@ -63,8 +66,10 @@ let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
const canvas = canvasEl.value!;
- canvas.width = canvas.offsetWidth;
- canvas.height = canvas.offsetHeight;
+ let width = canvas.offsetWidth;
+ let height = canvas.offsetHeight;
+ canvas.width = width;
+ canvas.height = height;
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (gl == null) return;
@@ -197,6 +202,7 @@ onMounted(() => {
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
}
`);
+ if (shaderProgram == null) return;
gl.useProgram(shaderProgram);
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
@@ -226,7 +232,23 @@ onMounted(() => {
gl!.uniform1f(u_time, 0);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
} else {
- function render(timeStamp) {
+ 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(u_resolution, [width, height]);
+ gl.viewport(0, 0, width, height);
+ }
+
gl!.uniform1f(u_time, timeStamp);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 9596ce6077..60978eb0bd 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -43,6 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
fixed
:instant="true"
:initialText="c.form.text"
+ :initialCw="c.form.cw"
/>
</div>
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
@@ -60,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { Ref } from 'vue';
+import { Ref, ref } from 'vue';
import * as os from '@/os.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -87,16 +88,17 @@ function g(id) {
return props.components.find(x => x.value.id === id).value;
}
-let valueForSwitch = $ref(c.default ?? false);
+const valueForSwitch = ref(c.default ?? false);
function onSwitchUpdate(v) {
- valueForSwitch = v;
+ valueForSwitch.value = v;
if (c.onChange) c.onChange(v);
}
function openPostForm() {
os.post({
initialText: c.form.text,
+ initialCw: c.form.cw,
instant: true,
});
}
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 9e92c4bb03..1f819cf601 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -242,29 +242,7 @@ function exec() {
return;
}
- const matched: EmojiDef[] = [];
- const max = 30;
-
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !x.aliasOf && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
-
- if (matched.length < max) {
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().startsWith(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
- }
-
- if (matched.length < max) {
- emojiDb.value.some(x => {
- if (x.name.toLowerCase().includes(props.q ? props.q.toLowerCase() : '') && !matched.some(y => y.emoji.toLowerCase() === x.emoji.toLowerCase())) matched.push(x);
- return matched.length === max;
- });
- }
-
- emojis.value = matched;
+ emojis.value = emojiAutoComplete(props.q.toLowerCase(), emojiDb.value);
} else if (props.type === 'mfmTag') {
if (!props.q || props.q === '') {
mfmTags.value = MFM_TAGS;
@@ -275,6 +253,78 @@ function exec() {
}
}
+type EmojiScore = { emoji: EmojiDef, score: number };
+
+function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
+ if (!query) {
+ return [];
+ }
+
+ const matched = new Map<string, EmojiScore>();
+
+ // 前方一致(エイリアスなし)
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) {
+ matched.set(x.name, { emoji: x, score: query.length + 1 });
+ }
+ return matched.size === max;
+ });
+
+ // 前方一致(エイリアス込み)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
+ }
+ return matched.size === max;
+ });
+ }
+
+ // 部分一致(エイリアス込み)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
+ }
+ return matched.size === max;
+ });
+ }
+
+ // 簡易あいまい検索(3文字以上)
+ if (matched.size < max && query.length > 3) {
+ const queryChars = [...query];
+ const hitEmojis = new Map<string, EmojiScore>();
+
+ for (const x of emojiDb) {
+ // 文字列の位置を進めながら、クエリの文字を順番に探す
+
+ let pos = 0;
+ let hit = 0;
+ for (const c of queryChars) {
+ pos = x.name.toLowerCase().indexOf(c, pos);
+ if (pos <= -1) break;
+ hit++;
+ }
+
+ // 半分以上の文字が含まれていればヒットとする
+ if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
+ hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
+ }
+ }
+
+ // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
+ [...hitEmojis.values()]
+ .sort((x, y) => y.score - x.score)
+ .slice(0, 6)
+ .forEach(it => matched.set(it.emoji.name, it));
+ }
+
+ return [...matched.values()]
+ .sort((x, y) => y.score - x.score)
+ .slice(0, max)
+ .map(it => it.emoji);
+}
+
function onMousedown(event: Event) {
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
}
@@ -309,12 +359,25 @@ function onKeydown(event: KeyboardEvent) {
}
break;
- case 'Tab':
case 'ArrowDown':
cancel();
selectNext();
break;
+ case 'Tab':
+ if (event.shiftKey) {
+ if (select.value !== -1) {
+ cancel();
+ selectPrev();
+ } else {
+ props.close();
+ }
+ } else {
+ cancel();
+ selectNext();
+ }
+ break;
+
default:
event.stopPropagation();
props.textarea.focus();
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 2fdc2bbe07..9fcc49d3f0 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onMounted } from 'vue';
+import { nextTick, onMounted, shallowRef } from 'vue';
const props = defineProps<{
type?: 'button' | 'submit' | 'reset';
@@ -59,13 +59,13 @@ const emit = defineEmits<{
(ev: 'click', payload: MouseEvent): void;
}>();
-let el = $shallowRef<HTMLElement | null>(null);
-let ripples = $shallowRef<HTMLElement | null>(null);
+const el = shallowRef<HTMLElement | null>(null);
+const ripples = shallowRef<HTMLElement | null>(null);
onMounted(() => {
if (props.autofocus) {
nextTick(() => {
- el!.focus();
+ el.value!.focus();
});
}
});
@@ -88,11 +88,11 @@ function onMousedown(evt: MouseEvent): void {
const rect = target.getBoundingClientRect();
const ripple = document.createElement('div');
- ripple.classList.add(ripples!.dataset.childrenClass!);
+ ripple.classList.add(ripples.value!.dataset.childrenClass!);
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
- ripples!.appendChild(ripple);
+ ripples.value!.appendChild(ripple);
const circleCenterX = evt.clientX - rect.left;
const circleCenterY = evt.clientY - rect.top;
@@ -107,7 +107,7 @@ function onMousedown(evt: MouseEvent): void {
ripple.style.opacity = '0';
}, 1000);
window.setTimeout(() => {
- if (ripples) ripples.removeChild(ripple);
+ if (ripples.value) ripples.value.removeChild(ripple);
}, 2000);
}
</script>
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 9c6e2f00bd..96590a469b 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1">
- <div class="banner" :style="bannerStyle">
- <div class="fade"></div>
- <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
- <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
- <div class="status">
- <div>
- <i class="ph-users ph-bold ph-lg"></i>
- <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
- <template #n>
- <b>{{ channel.usersCount }}</b>
- </template>
- </I18n>
- </div>
- <div>
- <i class="ph-pencil ph-bold ph-lg"></i>
- <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
- <template #n>
- <b>{{ channel.notesCount }}</b>
- </template>
- </I18n>
+<div style="position: relative;">
+ <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
+ <div class="banner" :style="bannerStyle">
+ <div class="fade"></div>
+ <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div>
+ <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
+ <div class="status">
+ <div>
+ <i class="ph-users ph-bold ph-lg"></i>
+ <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+ <template #n>
+ <b>{{ channel.usersCount }}</b>
+ </template>
+ </I18n>
+ </div>
+ <div>
+ <i class="ph-pencil ph-bold ph-lg"></i>
+ <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+ <template #n>
+ <b>{{ channel.notesCount }}</b>
+ </template>
+ </I18n>
+ </div>
</div>
</div>
- </div>
- <article v-if="channel.description">
- <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
- </article>
- <footer>
- <span v-if="channel.lastNotedAt">
- {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
- </span>
- </footer>
-</MkA>
+ <article v-if="channel.description">
+ <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
+ </article>
+ <footer>
+ <span v-if="channel.lastNotedAt">
+ {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+ </span>
+ </footer>
+ </MkA>
+ <div
+ v-if="channel.lastNotedAt && (channel.isFavorited || channel.isFollowing) && (!lastReadedAt || Date.parse(channel.lastNotedAt) > lastReadedAt)"
+ class="indicator"
+ ></div>
+</div>
</template>
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{
channel: Record<string, any>;
}>();
+const getLastReadedAt = (): number | null => {
+ return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null;
+};
+
+const lastReadedAt = ref(getLastReadedAt());
+
+watch(() => props.channel.id, () => {
+ lastReadedAt.value = getLastReadedAt();
+});
+
+const updateLastReadedAt = () => {
+ lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now();
+};
+
const bannerStyle = computed(() => {
if (props.channel.bannerUrl) {
return { backgroundImage: `url(${props.channel.bannerUrl})` };
@@ -170,4 +191,17 @@ const bannerStyle = computed(() => {
}
}
+.indicator {
+ position: absolute;
+ top: 0;
+ right: 0;
+ transform: translate(25%, -25%);
+ background-color: var(--accent);
+ border: solid var(--bg) 4px;
+ border-radius: 100%;
+ width: 1.5rem;
+ height: 1.5rem;
+ aspect-ratio: 1 / 1;
+}
+
</style>
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index fe7077bdbf..adb3c134ae 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -74,7 +74,7 @@ const props = defineProps({
},
});
-let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
+const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>();
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
const negate = arr => arr.map(x => -x);
@@ -268,7 +268,7 @@ const render = () => {
gradient,
},
},
- plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])],
+ plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
});
};
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 546bc0b4b1..c265fe6e97 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -13,29 +13,30 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { shallowRef } from 'vue';
import { Chart, LegendItem } from 'chart.js';
const props = defineProps({
});
-let chart = $shallowRef<Chart>();
-let items = $shallowRef<LegendItem[]>([]);
+const chart = shallowRef<Chart>();
+const items = shallowRef<LegendItem[]>([]);
function update(_chart: Chart, _items: LegendItem[]) {
- chart = _chart,
- items = _items;
+ chart.value = _chart,
+ items.value = _items;
}
function onClick(item: LegendItem) {
- if (chart == null) return;
- const { type } = chart.config;
+ if (chart.value == null) return;
+ const { type } = chart.value.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
- chart.toggleDataVisibility(item.index);
+ chart.value.toggleDataVisibility(item.index);
} else {
- chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex));
+ chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
}
- chart.update();
+ chart.value.update();
}
defineExpose({
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 71914c6886..1e72319010 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, onUnmounted } from 'vue';
+import { computed, onMounted, onUnmounted, ref } from 'vue';
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
@@ -29,8 +29,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
const saveData = game.saveData;
const cookies = computed(() => saveData.value?.cookies);
-let cps = $ref(0);
-let prevCookies = $ref(0);
+const cps = ref(0);
+const prevCookies = ref(0);
function onClick(ev: MouseEvent) {
const x = ev.clientX;
@@ -48,9 +48,9 @@ function onClick(ev: MouseEvent) {
}
useInterval(() => {
- const diff = saveData.value!.cookies - prevCookies;
- cps = diff;
- prevCookies = saveData.value!.cookies;
+ const diff = saveData.value!.cookies - prevCookies.value;
+ cps.value = diff;
+ prevCookies.value = saveData.value!.cookies;
}, 1000, {
immediate: false,
afterMounted: true,
@@ -63,7 +63,7 @@ useInterval(game.save, 1000 * 5, {
onMounted(async () => {
await game.load();
- prevCookies = saveData.value!.cookies;
+ prevCookies.value = saveData.value!.cookies;
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 21684b462a..19418cd4da 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -54,7 +54,7 @@ watch(() => props.lang, (to) => {
return new Promise((resolve) => {
fetchLanguage(to).then(() => resolve);
});
-}, { immediate: true, });
+}, { immediate: true });
</script>
<style scoped lang="scss">
@@ -62,7 +62,7 @@ watch(() => props.lang, (to) => {
padding: 1em;
margin: .5em 0;
overflow: auto;
- border-radius: .3em;
+ border-radius: 8px;
& pre,
& code {
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index b39e6ff23c..2c016e4d7c 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -4,18 +4,27 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
- <Suspense>
- <template #fallback>
- <MkLoading v-if="!inline ?? true" />
- </template>
- <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
- <XCode v-else :code="code" :lang="lang"/>
- </Suspense>
+<Suspense>
+ <template #fallback>
+ <MkLoading v-if="!inline ?? true"/>
+ </template>
+ <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
+ <XCode v-else-if="show && lang" :code="code" :lang="lang"/>
+ <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
+ <button v-else :class="$style.codePlaceholderRoot" @click="show = true">
+ <div :class="$style.codePlaceholderContainer">
+ <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
+ <div>{{ i18n.ts.clickToShow }}</div>
+ </div>
+ </button>
+</Suspense>
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import MkLoading from '@/components/global/MkLoading.vue';
+import { defaultStore } from '@/store.js';
+import { i18n } from '@/i18n.js';
defineProps<{
code: string;
@@ -23,6 +32,8 @@ defineProps<{
inline?: boolean;
}>();
+const show = ref(!defaultStore.state.dataSaver.code);
+
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
</script>
@@ -36,4 +47,42 @@ const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'))
padding: .1em;
border-radius: .3em;
}
+
+.codeBlockFallbackRoot {
+ display: block;
+ overflow-wrap: anywhere;
+ color: #D4D4D4;
+ background: #1E1E1E;
+ padding: 1em;
+ margin: .5em 0;
+ overflow: auto;
+ border-radius: 8px;
+}
+
+.codeBlockFallbackCode {
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+}
+
+.codePlaceholderRoot {
+ display: block;
+ width: 100%;
+ background: none;
+ border: none;
+ outline: none;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+
+ box-sizing: border-box;
+ border-radius: 8px;
+ padding: 24px;
+ margin-top: 4px;
+ color: #D4D4D4;
+ background: #1E1E1E;
+}
+
+.codePlaceholderContainer {
+ text-align: center;
+ font-size: 0.8em;
+}
</style>
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index 5434042684..c9bcc71196 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -4,30 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.codeEditorRoot, { [$style.disabled]: disabled, [$style.focused]: focused }]">
- <div :class="$style.codeEditorScroller">
- <textarea
- ref="inputEl"
- v-model="vModel"
- :class="[$style.textarea]"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- autocomplete="off"
- wrap="off"
- spellcheck="false"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- ></textarea>
- <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
+<div>
+ <div :class="$style.label" @click="focus"><slot name="label"></slot></div>
+ <div :class="[$style.codeEditorRoot, { [$style.focused]: focused }]">
+ <div :class="$style.codeEditorScroller">
+ <textarea
+ ref="inputEl"
+ v-model="vModel"
+ :class="[$style.textarea]"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ autocomplete="off"
+ wrap="off"
+ spellcheck="false"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ ></textarea>
+ <XCode :class="$style.codeEditorHighlighter" :codeEditor="true" :code="v" :lang="lang"/>
+ </div>
</div>
+ <div :class="$style.caption"><slot name="caption"></slot></div>
+ <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, toRefs, shallowRef, nextTick } from 'vue';
+import { debounce } from 'throttle-debounce';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n.js';
import XCode from '@/components/MkCode.core.vue';
const props = withDefaults(defineProps<{
@@ -36,6 +44,8 @@ const props = withDefaults(defineProps<{
required?: boolean;
readonly?: boolean;
disabled?: boolean;
+ debounce?: boolean;
+ manualSave?: boolean;
}>(), {
lang: 'js',
});
@@ -54,6 +64,8 @@ const focused = ref(false);
const changed = ref(false);
const inputEl = shallowRef<HTMLTextAreaElement>();
+const focus = () => inputEl.value?.focus();
+
const onInput = (ev) => {
v.value = ev.target?.value ?? v.value;
changed.value = true;
@@ -100,16 +112,48 @@ const updated = () => {
emit('update:modelValue', v.value);
};
+const debouncedUpdated = debounce(1000, updated);
+
watch(modelValue, newValue => {
v.value = newValue ?? '';
});
-watch(v, () => {
- updated();
+watch(v, newValue => {
+ if (!props.manualSave) {
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
+ }
});
</script>
<style lang="scss" module>
+.label {
+ font-size: 0.85em;
+ padding: 0 0 8px 0;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.caption {
+ font-size: 0.85em;
+ padding: 8px 0 0 0;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
+ }
+}
+
+.save {
+ margin: 8px 0 0 0;
+}
+
.codeEditorRoot {
min-width: 100%;
max-width: 100%;
@@ -117,6 +161,7 @@ watch(v, () => {
overflow-y: hidden;
box-sizing: border-box;
margin: 0;
+ border-radius: 6px;
padding: 0;
color: var(--fg);
border: solid 1px var(--panel);
@@ -139,6 +184,10 @@ watch(v, () => {
height: 100%;
}
+.textarea, .codeEditorHighlighter {
+ margin: 0;
+}
+
.textarea {
position: absolute;
top: 0;
@@ -153,7 +202,10 @@ watch(v, () => {
caret-color: rgb(225, 228, 232);
background-color: transparent;
border: 0;
+ border-radius: 6px;
outline: 0;
+ min-width: calc(100% - 24px);
+ height: 100%;
padding: 12px;
line-height: 1.5em;
font-size: 1em;
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 79b1949640..4f15e88951 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -24,8 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
-import { i18n } from '@/i18n.js';
+import { ref, shallowRef, toRefs } from 'vue';
const props = defineProps<{
modelValue: string | null;
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index 6cca7fc353..b78252be89 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onBeforeUnmount } from 'vue';
+import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains.js';
@@ -34,9 +34,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let rootEl = $shallowRef<HTMLDivElement>();
+const rootEl = shallowRef<HTMLDivElement>();
-let zIndex = $ref<number>(os.claimZIndex('high'));
+const zIndex = ref<number>(os.claimZIndex('high'));
const SCROLLBAR_THICKNESS = 16;
@@ -44,8 +44,8 @@ onMounted(() => {
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
- const width = rootEl.offsetWidth;
- const height = rootEl.offsetHeight;
+ const width = rootEl.value.offsetWidth;
+ const height = rootEl.value.offsetHeight;
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
@@ -63,8 +63,8 @@ onMounted(() => {
left = 0;
}
- rootEl.style.top = `${top}px`;
- rootEl.style.left = `${left}px`;
+ rootEl.value.style.top = `${top}px`;
+ rootEl.value.style.left = `${left}px`;
document.body.addEventListener('mousedown', onMousedown);
});
@@ -74,7 +74,7 @@ onBeforeUnmount(() => {
});
function onMousedown(evt: Event) {
- if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
+ if (!contains(rootEl.value, evt.target) && (rootEl.value !== evt.target)) emit('closed');
}
</script>
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 81f3936600..0a1ddd3171 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
@@ -56,10 +56,10 @@ const props = defineProps<{
}>();
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
-let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
-let imgEl = $shallowRef<HTMLImageElement>();
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const imgEl = shallowRef<HTMLImageElement>();
let cropper: Cropper | null = null;
-let loading = $ref(true);
+const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
@@ -94,16 +94,16 @@ const ok = async () => {
const f = await promise;
emit('ok', f);
- dialogEl!.close();
+ dialogEl.value!.close();
};
const cancel = () => {
emit('cancel');
- dialogEl!.close();
+ dialogEl.value!.close();
};
const onImageLoad = () => {
- loading = false;
+ loading.value = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
@@ -112,7 +112,7 @@ const onImageLoad = () => {
};
onMounted(() => {
- cropper = new Cropper(imgEl!, {
+ cropper = new Cropper(imgEl.value!, {
});
const computedStyle = getComputedStyle(document.documentElement);
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index 0cdaf7c9bd..4a6d2dfba2 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -16,7 +16,23 @@ import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
modelValue: boolean;
- note: Misskey.entities.Note;
+ text: string | null;
+ renote: Misskey.entities.Note | null;
+ files: Misskey.entities.DriveFile[];
+ poll?: {
+ expiresAt: string | null;
+ multiple: boolean;
+ choices: {
+ isVoted: boolean;
+ text: string;
+ votes: number;
+ }[];
+ } | {
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string | null;
+ expiredAfter: string | null;
+ };
}>();
const emit = defineEmits<{
@@ -25,9 +41,10 @@ const emit = defineEmits<{
const label = computed(() => {
return concat([
- props.note.text ? [i18n.t('_cw.chars', { count: props.note.text.length })] : [],
- props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length })] : [],
- props.note.poll != null ? [i18n.ts.poll] : [],
+ props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
+ props.renote ? [i18n.ts.quote] : [],
+ props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
+ props.poll != null ? [i18n.ts.poll] : [],
] as string[][]).join(' / ');
});
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index e0692eb383..2c0f6a4d78 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
<template #caption>
- <span v-if="okButtonDisabled && disabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
- <span v-else-if="okButtonDisabled && disabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
+ <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
+ <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</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="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+ <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @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>
</div>
<div v-if="actions" :class="$style.buttons">
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
+import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -122,24 +122,21 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null);
-let disabledReason = $ref<null | 'charactersExceeded' | 'charactersBelow'>(null);
-const okButtonDisabled = $computed<boolean>(() => {
+const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
if (props.input.minLength) {
if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
- disabledReason = 'charactersBelow';
- return true;
+ return 'charactersBelow';
}
}
if (props.input.maxLength) {
if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) {
- disabledReason = 'charactersExceeded';
- return true;
+ return 'charactersExceeded';
}
}
}
- return false;
+ return null;
});
function done(canceled: boolean, result?) {
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 9f79a44d4c..dcaaa72cf4 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -39,6 +39,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
@@ -250,7 +251,7 @@ function setAsUploadFolder() {
}
function onContextmenu(ev: MouseEvent) {
- let menu;
+ let menu: MenuItem[];
menu = [{
text: i18n.ts.openInWindow,
icon: 'ph-app-window ph-bold ph-lg',
@@ -260,18 +261,18 @@ function onContextmenu(ev: MouseEvent) {
}, {
}, 'closed');
},
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.rename,
icon: 'ph-textbox ph-bold ph-lg',
action: rename,
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.delete,
icon: 'ph-trash ph-bold ph-lg',
danger: true,
action: deleteFolder,
}];
if (defaultStore.state.devMode) {
- menu = menu.concat([null, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ph-identification-card ph-bold ph-lg',
text: i18n.ts.copyFolderId,
action: () => {
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 5281541927..00bb0e6e2b 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -616,7 +616,7 @@ function getMenu() {
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
- }, null, {
+ }, { type: 'divider' }, {
text: i18n.ts.addFile,
type: 'label',
}, {
@@ -627,7 +627,7 @@ function getMenu() {
text: i18n.ts.fromUrl,
icon: 'ph-link ph-bold ph-lg',
action: () => { urlUpload(); },
- }, null, {
+ }, { type: 'divider' }, {
text: folder.value ? folder.value.name : i18n.ts.drive,
type: 'label',
}, folder.value ? {
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 00e0a0e042..49c146b68d 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
-<section>
+<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
+<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
<header class="_acrylic" @click="shown = !shown">
- <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> ({{ emojis.length }})
+ <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-bold ph-lg"></i>:{{ emojis.length }})
</header>
<div v-if="shown" class="body">
<button
@@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</div>
</section>
+<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
+<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
+ <header class="_acrylic" @click="shown = !shown">
+ <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
+ </header>
+ <div v-if="shown" style="padding-left: 9px;">
+ <MkEmojiPickerSection
+ v-for="child in customEmojiTree"
+ :key="`custom:${child.value}`"
+ :initialShown="initialShown"
+ :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
+ :hasChildSection="child.children.length !== 0"
+ :customEmojiTree="child.children"
+ @chosen="nestedChosen"
+ >
+ {{ child.value || i18n.ts.other }}
+ </MkEmojiPickerSection>
+ </div>
+ <div v-if="shown" class="body">
+ <button
+ v-for="emoji in emojis"
+ :key="emoji"
+ :data-emoji="emoji"
+ class="_button item"
+ @pointerenter="computeButtonTitle"
+ @click="emit('chosen', emoji, $event)"
+ >
+ <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
+ <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
+ </button>
+ </div>
+</section>
</template>
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
-import { getEmojiName } from '@/scripts/emojilist.js';
+import { i18n } from '../i18n.js';
+import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { customEmojis } from '@/custom-emojis.js';
+import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
const props = defineProps<{
emojis: string[] | Ref<string[]>;
initialShown?: boolean;
+ hasChildSection?: boolean;
+ customEmojiTree?: CustomEmojiFolderTree[];
}>();
const emit = defineEmits<{
@@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji) ?? emoji;
}
+function nestedChosen(emoji: any, ev?: MouseEvent) {
+ emit('chosen', emoji, ev);
+}
</script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 50ed8048bb..b7e329d7c2 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</section>
<div v-if="tab === 'index'" class="group index">
- <section v-if="showPinned">
+ <section v-if="showPinned && pinned.length > 0">
<div class="body">
<button
v-for="emoji in pinned"
@@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection
- v-for="category in customEmojiCategories"
- :key="`custom:${category}`"
+ v-for="child in customEmojiFolderRoot.children"
+ :key="`custom:${child.value}`"
:initialShown="false"
- :emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
+ :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
+ :hasChildSection="child.children.length !== 0"
+ :customEmojiTree="child.children"
@chosen="chosen"
>
- {{ category || i18n.ts.other }}
+ {{ child.value || i18n.ts.other }}
</XSection>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
- <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
+ <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue';
-import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist.js';
+import {
+ emojilist,
+ emojiCharByCategory,
+ UnicodeEmojiDef,
+ unicodeEmojiCategories as categories,
+ getEmojiName,
+ CustomEmojiFolderTree,
+} from '@/scripts/emojilist.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
@@ -112,10 +121,11 @@ import { $i } from '@/account.js';
const props = withDefaults(defineProps<{
showPinned?: boolean;
- asReactionPicker?: boolean;
+ pinnedEmojis?: string[];
maxHeight?: number;
asDrawer?: boolean;
asWindow?: boolean;
+ asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
}>(), {
showPinned: true,
});
@@ -128,22 +138,50 @@ const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();
const {
- reactions: pinned,
- reactionPickerSize,
- reactionPickerWidth,
- reactionPickerHeight,
- disableShowingAnimatedImages,
+ emojiPickerScale,
+ emojiPickerWidth,
+ emojiPickerHeight,
recentlyUsedEmojis,
} = defaultStore.reactiveState;
-const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1);
-const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
-const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
+const pinned = computed(() => props.pinnedEmojis);
+const size = computed(() => emojiPickerScale.value);
+const width = computed(() => emojiPickerWidth.value);
+const height = computed(() => emojiPickerHeight.value);
const q = ref<string>('');
-const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
+const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
+const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };
+
+function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
+ const parts = input.split('/').map(p => p.trim());
+ let currentNode: CustomEmojiFolderTree = root;
+
+ for (const part of parts) {
+ let existingNode = currentNode.children.find((node) => node.value === part);
+
+ if (!existingNode) {
+ const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
+ currentNode.children.push(newNode);
+ existingNode = newNode;
+ }
+
+ currentNode = existingNode;
+ }
+
+ return currentNode;
+}
+
+customEmojiCategories.value.forEach(ec => {
+ if (ec !== null) {
+ parseAndMergeCategories(ec, customEmojiFolderRoot);
+ }
+});
+
+parseAndMergeCategories('', customEmojiFolderRoot);
+
watch(q, () => {
if (emojisEl.value) emojisEl.value.scrollTop = 0;
@@ -158,7 +196,7 @@ watch(q, () => {
const searchCustom = () => {
const max = 100;
const emojis = customEmojis.value;
- const matches = new Set<Misskey.entities.CustomEmoji>();
+ const matches = new Set<Misskey.entities.EmojiSimple>();
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
@@ -288,7 +326,7 @@ watch(q, () => {
searchResultUnicode.value = Array.from(searchUnicode());
});
-function filterAvailable(emoji: Misskey.entities.CustomEmoji): boolean {
+function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
}
@@ -305,7 +343,7 @@ function reset() {
q.value = '';
}
-function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
+function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): string {
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
}
@@ -329,7 +367,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
emit('chosen', key);
// 最近使った絵文字更新
- if (!pinned.value.includes(key)) {
+ if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents.unshift(key);
@@ -572,8 +610,7 @@ defineExpose({
position: sticky;
top: 0;
left: 0;
- height: 32px;
- line-height: 32px;
+ line-height: 28px;
z-index: 1;
padding: 0 8px;
font-size: 12px;
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 581d815d66..4068a79f08 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal"
v-slot="{ type, maxHeight }"
:zPriority="'middle'"
- :preferType="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
:transparentBg="true"
:manualShowing="manualShowing"
:src="src"
@@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_popup _shadow"
:class="{ [$style.drawer]: type === 'drawer' }"
:showPinned="showPinned"
+ :pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker"
:asDrawer="type === 'drawer'"
:max-height="maxHeight"
@@ -36,15 +37,19 @@ import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
import { defaultStore } from '@/store.js';
-withDefaults(defineProps<{
+const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
src?: HTMLElement;
showPinned?: boolean;
+ pinnedEmojis?: string[],
asReactionPicker?: boolean;
+ choseAndClose?: boolean;
}>(), {
manualShowing: null,
showPinned: true,
+ pinnedEmojis: undefined,
asReactionPicker: false,
+ choseAndClose: true,
});
const emit = defineEmits<{
@@ -58,7 +63,9 @@ const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) {
emit('done', emoji);
- modal.value?.close();
+ if (props.choseAndClose) {
+ modal.value?.close();
+ }
}
function opening() {
diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue
index cef1943d5c..6d1bad7433 100644
--- a/packages/frontend/src/components/MkFeaturedPhotos.vue
+++ b/packages/frontend/src/components/MkFeaturedPhotos.vue
@@ -12,7 +12,7 @@ import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
+const meta = ref<Misskey.entities.MetaResponse>();
os.api('meta', { detail: true }).then(gotMeta => {
meta.value = gotMeta;
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index b582b88712..b799fb9447 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -42,12 +42,12 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-let caption = $ref(props.default);
+const caption = ref(props.default);
async function ok() {
- emit('done', caption);
- dialog.close();
+ emit('done', caption.value);
+ dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 30e93ef9e4..03621a4255 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, onMounted } from 'vue';
+import { nextTick, onMounted, shallowRef, ref } from 'vue';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
@@ -70,10 +70,10 @@ const getBgColor = (el: HTMLElement) => {
}
};
-let rootEl = $shallowRef<HTMLElement>();
-let bgSame = $ref(false);
-let opened = $ref(props.defaultOpen);
-let openedAtLeastOnce = $ref(props.defaultOpen);
+const rootEl = shallowRef<HTMLElement>();
+const bgSame = ref(false);
+const opened = ref(props.defaultOpen);
+const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) {
const elementHeight = el.getBoundingClientRect().height;
@@ -98,20 +98,20 @@ function afterLeave(el) {
}
function toggle() {
- if (!opened) {
- openedAtLeastOnce = true;
+ if (!opened.value) {
+ openedAtLeastOnce.value = true;
}
nextTick(() => {
- opened = !opened;
+ opened.value = !opened.value;
});
}
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
- const parentBg = getBgColor(rootEl.parentElement);
+ const parentBg = getBgColor(rootEl.value.parentElement);
const myBg = computedStyle.getPropertyValue('--panel');
- bgSame = parentBg === myBg;
+ bgSame.value = parentBg === myBg;
});
</script>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index eebb753db1..d1b1956a03 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted } from 'vue';
+import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { useStream } from '@/stream.js';
@@ -57,9 +57,9 @@ const emit = defineEmits<{
(_: 'update:user', value: Misskey.entities.UserDetailed): void
}>();
-let isFollowing = $ref(props.user.isFollowing);
-let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
-let wait = $ref(false);
+const isFollowing = ref(props.user.isFollowing);
+const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
+const wait = ref(false);
const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) {
@@ -71,16 +71,16 @@ if (props.user.isFollowing == null) {
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id === props.user.id) {
- isFollowing = user.isFollowing;
- hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
+ isFollowing.value = user.isFollowing;
+ hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
}
}
async function onClick() {
- wait = true;
+ wait.value = true;
try {
- if (isFollowing) {
+ if (isFollowing.value) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
@@ -92,11 +92,11 @@ async function onClick() {
userId: props.user.id,
});
} else {
- if (hasPendingFollowRequestFromYou) {
+ if (hasPendingFollowRequestFromYou.value) {
await os.api('following/requests/cancel', {
userId: props.user.id,
});
- hasPendingFollowRequestFromYou = false;
+ hasPendingFollowRequestFromYou.value = false;
} else {
await os.api('following/create', {
userId: props.user.id,
@@ -104,9 +104,9 @@ async function onClick() {
});
emit('update:user', {
...props.user,
- withReplies: defaultStore.state.defaultWithReplies
+ withReplies: defaultStore.state.defaultWithReplies,
});
- hasPendingFollowRequestFromYou = true;
+ hasPendingFollowRequestFromYou.value = true;
claimAchievement('following1');
@@ -127,7 +127,7 @@ async function onClick() {
} catch (err) {
console.error(err);
} finally {
- wait = false;
+ wait.value = false;
}
}
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 521ac11d12..9b57688a02 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -53,19 +53,19 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let dialog: InstanceType<typeof MkModalWindow> = $ref();
+const dialog = ref<InstanceType<typeof MkModalWindow>>();
-let username = $ref('');
-let email = $ref('');
-let processing = $ref(false);
+const username = ref('');
+const email = ref('');
+const processing = ref(false);
async function onSubmit() {
- processing = true;
+ processing.value = true;
await os.apiWithDialog('request-reset-password', {
- username,
- email,
+ username: username.value,
+ email: email.value,
});
emit('done');
- dialog.close();
+ dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 24404728ca..6f882cfab7 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput>
- <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
+ <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkInput>
- <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
+ <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</MkTextarea>
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index 185a49b5a9..c0b20507fc 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -23,7 +23,7 @@ const query = ref(props.q);
const search = () => {
const sp = new URLSearchParams();
sp.append('q', query.value);
- window.open(`https://www.google.com/search?${sp.toString()}`, '_blank');
+ window.open(`https://www.google.com/search?${sp.toString()}`, '_blank', 'noopener');
};
</script>
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 0022531e58..a57e6c9292 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, watch } from 'vue';
+import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -27,11 +27,11 @@ const props = defineProps<{
src: string;
}>();
-const rootEl = $shallowRef<HTMLDivElement>(null);
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
@@ -42,8 +42,8 @@ async function renderChart() {
chartInstance.destroy();
}
- const wide = rootEl.offsetWidth > 700;
- const narrow = rootEl.offsetWidth < 400;
+ const wide = rootEl.value.offsetWidth > 700;
+ const narrow = rootEl.value.offsetWidth < 400;
const weeks = wide ? 50 : narrow ? 10 : 25;
const chartLimit = 7 * weeks;
@@ -88,7 +88,7 @@ async function renderChart() {
values = raw.deliverFailed;
}
- fetching = false;
+ fetching.value = false;
await nextTick();
@@ -101,7 +101,7 @@ async function renderChart() {
const marginEachCell = 4;
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
@@ -210,7 +210,7 @@ async function renderChart() {
}
watch(() => props.src, () => {
- fetching = true;
+ fetching.value = true;
renderChart();
});
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 4fb573fdbc..942861e1f4 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { $ref } from 'vue/macros';
import DrawBlurhash from '@/workers/draw-blurhash?worker';
import TestWebGL2 from '@/workers/test-webgl2?worker';
import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
@@ -58,7 +57,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
</script>
<script lang="ts" setup>
-import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
+import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { render } from 'buraha';
import { defaultStore } from '@/store.js';
@@ -98,41 +97,41 @@ const viewId = uuid();
const canvas = shallowRef<HTMLCanvasElement>();
const root = shallowRef<HTMLDivElement>();
const img = shallowRef<HTMLImageElement>();
-let loaded = $ref(false);
-let canvasWidth = $ref(64);
-let canvasHeight = $ref(64);
-let imgWidth = $ref(props.width);
-let imgHeight = $ref(props.height);
-let bitmapTmp = $ref<CanvasImageSource | undefined>();
-const hide = computed(() => !loaded || props.forceBlurhash);
+const loaded = ref(false);
+const canvasWidth = ref(64);
+const canvasHeight = ref(64);
+const imgWidth = ref(props.width);
+const imgHeight = ref(props.height);
+const bitmapTmp = ref<CanvasImageSource | undefined>();
+const hide = computed(() => !loaded.value || props.forceBlurhash);
function waitForDecode() {
if (props.src != null && props.src !== '') {
nextTick()
.then(() => img.value?.decode())
.then(() => {
- loaded = true;
+ loaded.value = true;
}, error => {
console.log('Error occurred during decoding image', img.value, error);
});
} else {
- loaded = false;
+ loaded.value = false;
}
}
watch([() => props.width, () => props.height, root], () => {
const ratio = props.width / props.height;
if (ratio > 1) {
- canvasWidth = Math.round(64 * ratio);
- canvasHeight = 64;
+ canvasWidth.value = Math.round(64 * ratio);
+ canvasHeight.value = 64;
} else {
- canvasWidth = 64;
- canvasHeight = Math.round(64 / ratio);
+ canvasWidth.value = 64;
+ canvasHeight.value = Math.round(64 / ratio);
}
const clientWidth = root.value?.clientWidth ?? 300;
- imgWidth = clientWidth;
- imgHeight = Math.round(clientWidth / ratio);
+ imgWidth.value = clientWidth;
+ imgHeight.value = Math.round(clientWidth / ratio);
}, {
immediate: true,
});
@@ -140,15 +139,15 @@ watch([() => props.width, () => props.height, root], () => {
function drawImage(bitmap: CanvasImageSource) {
// canvasがない(mountedされていない)場合はTmpに保存しておく
if (!canvas.value) {
- bitmapTmp = bitmap;
+ bitmapTmp.value = bitmap;
return;
}
// canvasがあれば描画する
- bitmapTmp = undefined;
+ bitmapTmp.value = undefined;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
- ctx.drawImage(bitmap, 0, 0, canvasWidth, canvasHeight);
+ ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value);
}
function drawAvg() {
@@ -160,7 +159,7 @@ function drawAvg() {
// avgColorでお茶をにごす
ctx.beginPath();
ctx.fillStyle = extractAvgColorFromBlurhash(props.hash) ?? '#888';
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+ ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
}
async function draw() {
@@ -212,8 +211,8 @@ watch(() => props.hash, () => {
onMounted(() => {
// drawImageがmountedより先に呼ばれている場合はここで描画する
- if (bitmapTmp) {
- drawImage(bitmapTmp);
+ if (bitmapTmp.value) {
+ drawImage(bitmapTmp.value);
}
waitForDecode();
});
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 6f237761a8..b4b4e1b0b7 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -43,11 +43,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
+import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: string | number | null;
@@ -59,6 +60,7 @@ const props = defineProps<{
placeholder?: string;
autofocus?: boolean;
autocomplete?: string;
+ mfmAutocomplete?: boolean | SuggestionType[],
autocapitalize?: string;
spellcheck?: boolean;
step?: any;
@@ -93,6 +95,7 @@ const height =
props.small ? 33 :
props.large ? 39 :
36;
+let autocomplete: Autocomplete;
const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => {
@@ -160,6 +163,16 @@ onMounted(() => {
focus();
}
});
+
+ if (props.mfmAutocomplete) {
+ autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+ }
+});
+
+onUnmounted(() => {
+ if (autocomplete) {
+ autocomplete.detach();
+ }
});
defineExpose({
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 6af9c6ccb5..9cde197e19 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -15,21 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
- instance: Misskey.entities.Instance;
+ instance: Misskey.entities.FederationInstance;
}>();
-let chartValues = $ref<number[] | null>(null);
+const chartValues = ref<number[] | null>(null);
os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
- res.requests.received.splice(0, 1);
- chartValues = res.requests.received;
+ res['requests.received'].splice(0, 1);
+ chartValues.value = res['requests.received'];
});
function getInstanceIcon(instance): string {
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 509254de74..7b763ad385 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref, shallowRef } from 'vue';
import { Chart } from 'chart.js';
import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
@@ -100,11 +100,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
const chartLimit = 500;
-let chartSpan = $ref<'hour' | 'day'>('hour');
-let chartSrc = $ref('active-users');
-let heatmapSrc = $ref('active-users');
-let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
-let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
+const chartSpan = ref<'hour' | 'day'>('hour');
+const chartSrc = ref('active-users');
+const heatmapSrc = ref('active-users');
+const subDoughnutEl = shallowRef<HTMLCanvasElement>();
+const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
const { handler: externalTooltipHandler1 } = useChartTooltip({
position: 'middle',
@@ -163,7 +163,7 @@ function createDoughnut(chartEl, tooltip, data) {
onMounted(() => {
os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
- createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
+ createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followersCount,
@@ -172,7 +172,7 @@ onMounted(() => {
},
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
- createDoughnut(pubDoughnutEl, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
+ createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followingCount,
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index f0650e48f1..e358a1c549 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
-const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 8e3561e2b8..54d997d1c9 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -67,7 +67,7 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{
- invite: Misskey.entities.Invite;
+ invite: Misskey.entities.InviteCode;
moderator?: boolean;
}>();
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 17f8af4f63..099082f539 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
@@ -48,7 +48,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
deviceKind === 'smartphone' ? 'drawer' :
'dialog';
-const modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const menu = defaultStore.state.menu;
@@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
}));
function close() {
- modal.close();
+ modal.value.close();
}
</script>
@@ -101,6 +101,8 @@ function close() {
vertical-align: bottom;
height: 100px;
border-radius: var(--radius);
+ padding: 10px;
+ box-sizing: border-box;
&:hover {
color: var(--accent);
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 114b9b4faf..e16307c762 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component
- :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target"
+ :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target"
:title="url"
>
<slot></slot>
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import { url as local } from '@/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
@@ -29,13 +29,13 @@ const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
-const el = $ref();
+const el = ref();
-useTooltip($$(el), (showing) => {
+useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing,
url: props.url,
- source: el,
+ source: el.value,
}, {}, 'closed');
});
</script>
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 42a709ae26..4594c8a1db 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, shallowRef, watch } from 'vue';
+import { shallowRef, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
});
const audioEl = shallowRef<HTMLAudioElement>();
-let hide = $ref(true);
+const hide = ref(true);
watch(audioEl, () => {
if (audioEl.value) {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 1fa42c1e48..0040f00dc2 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<ImgWithBlurhash
:hash="image.blurhash"
- :src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
+ :src="(defaultStore.state.dataSaver.media && hide) ? null : url"
:forceBlurhash="hide"
:cover="hide || cover"
:alt="image.comment || image.name"
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="hide">
<div :class="$style.hiddenText">
<div :class="$style.hiddenTextWrapper">
- <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+ <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import bytes from '@/filters/bytes.js';
@@ -74,10 +74,10 @@ const props = withDefaults(defineProps<{
controls: true,
});
-let hide = $ref(true);
-let darkMode: boolean = $ref(defaultStore.state.darkMode);
+const hide = ref(true);
+const darkMode = ref<boolean>(defaultStore.state.darkMode);
-const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
+const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url
: defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url)
@@ -88,14 +88,14 @@ function onclick() {
if (!props.controls) {
return;
}
- if (hide) {
- hide = false;
+ if (hide.value) {
+ hide.value = false;
}
}
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
watch(() => props.image, () => {
- hide = (defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
+ hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
}, {
deep: true,
immediate: true,
@@ -106,7 +106,7 @@ function showMenu(ev: MouseEvent) {
text: i18n.ts.hide,
icon: 'ph-eye-slash ph-bold ph-lg',
action: () => {
- hide = true;
+ hide.value = true;
},
}, ...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 610978e4ab..46e32ef2d8 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div ref="root" :class="$style.root">
+<div :class="$style.root">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div
@@ -28,43 +28,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
-<script lang="ts">
-/**
- * アスペクト比算出のためにHTMLElement.clientWidthを使うが、
- * 大変重たいのでコンテナ要素とメディアリスト幅のペアをキャッシュする
- * (タイムラインごとにスクロールコンテナが存在する前提だが……)
- */
-const widthCache = new Map<Element, number>();
-
-/**
- * コンテナ要素がリサイズされたらキャッシュを削除する
- */
-const ro = new ResizeObserver(entries => {
- for (const entry of entries) {
- widthCache.delete(entry.target);
- }
-});
-
-async function getClientWidthWithCache(targetEl: HTMLElement, containerEl: HTMLElement, count = 0) {
- if (_DEV_) console.log('getClientWidthWithCache', { targetEl, containerEl, count, cache: widthCache.get(containerEl) });
- if (widthCache.has(containerEl)) return widthCache.get(containerEl)!;
-
- const width = targetEl.clientWidth;
-
- if (count <= 10 && width < 64) {
- // widthが64未満はおかしいのでリトライする
- await new Promise(resolve => setTimeout(resolve, 50));
- return getClientWidthWithCache(targetEl, containerEl, count + 1);
- }
-
- widthCache.set(containerEl, width);
- ro.observe(containerEl);
- return width;
-}
-</script>
-
<script lang="ts" setup>
-import { onMounted, onUnmounted, shallowRef } from 'vue';
+import { computed, onMounted, onUnmounted, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -76,19 +41,16 @@ import XModPlayer from '@/components/MkModPlayer.vue';
import * as os from '@/os.js';
import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js';
import { defaultStore } from '@/store.js';
-import { getScrollContainer, getBodyScrollHeight } from '@/scripts/scroll.js';
const props = defineProps<{
mediaList: Misskey.entities.DriveFile[];
raw?: boolean;
}>();
-const root = shallowRef<HTMLDivElement>();
-const container = shallowRef<HTMLElement | null | undefined>(undefined);
const gallery = shallowRef<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
-const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
+const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
let lightbox: PhotoSwipeLightbox | null;
const popstateHandler = (): void => {
@@ -97,12 +59,8 @@ const popstateHandler = (): void => {
}
};
-/**
- * アスペクト比をmediaListWithOneImageAppearanceに基づいていい感じに調整する
- * aspect-ratioではなくheightを使う
- */
async function calcAspectRatio() {
- if (!gallery.value || !root.value) return;
+ if (!gallery.value) return;
let img = props.mediaList[0];
@@ -111,41 +69,22 @@ async function calcAspectRatio() {
return;
}
- if (!container.value) container.value = getScrollContainer(root.value);
- const width = container.value ? await getClientWidthWithCache(root.value, container.value) : root.value.clientWidth;
-
- const heightMin = (ratio: number) => {
- const imgResizeRatio = width / img.properties.width;
- const imgDrawHeight = img.properties.height * imgResizeRatio;
- const maxHeight = width * ratio;
- const height = Math.min(imgDrawHeight, maxHeight);
- if (_DEV_) console.log('Image height calculated:', { width, properties: img.properties, imgResizeRatio, imgDrawHeight, maxHeight, height });
- return `${height}px`;
- };
+ const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9':
- gallery.value.style.height = heightMin(9 / 16);
+ gallery.value.style.aspectRatio = ratioMax(16 / 9);
break;
case '1_1':
- gallery.value.style.height = heightMin(1);
+ gallery.value.style.aspectRatio = ratioMax(1 / 1);
break;
case '2_3':
- gallery.value.style.height = heightMin(3 / 2);
+ gallery.value.style.aspectRatio = ratioMax(2 / 3);
break;
- default: {
- const maxHeight = Math.max(64, (container.value ? container.value.clientHeight : getBodyScrollHeight()) * 0.5 || 360);
- if (width === 0 || !maxHeight) return;
- const imgResizeRatio = width / img.properties.width;
- const imgDrawHeight = img.properties.height * imgResizeRatio;
- gallery.value.style.height = `${Math.max(64, Math.min(imgDrawHeight, maxHeight))}px`;
- gallery.value.style.minHeight = 'initial';
- gallery.value.style.maxHeight = 'initial';
+ default:
+ gallery.value.style.aspectRatio = '';
break;
- }
}
-
- gallery.value.style.aspectRatio = 'initial';
}
const isModule = (file: Misskey.entities.DriveFile): boolean => {
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 33a9b0fbf9..4f8560f0f1 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="hide" :class="[$style.hidden, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]" @click="hide = false">
<!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
<div :class="$style.sensitive">
- <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.enableDataSaverMode && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+ <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
@@ -37,18 +37,25 @@ import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import hasAudio from '@/scripts/media-has-audio.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
}>();
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
const videoEl = shallowRef<HTMLVideoElement>();
watch(videoEl, () => {
if (videoEl.value) {
videoEl.value.volume = 0.3;
+ hasAudio(videoEl.value).then(had => {
+ if (!had) {
+ videoEl.value.loop = videoEl.value.muted = true;
+ videoEl.value.play();
+ }
+ });
}
});
</script>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 83f56dc1a2..b0f997a1b9 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
- <div v-if="item === null" role="separator" :class="$style.divider"></div>
+ <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
- <span>{{ item.text }}</span>
+ <span style="opacity: 0.7;">{{ item.text }}</span>
</span>
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
<span><MkEllipsis/></span>
@@ -23,32 +23,44 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</MkA>
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</a>
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div v-if="item.indicate" :class="$style.item_content">
+ <span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</button>
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
- <span :class="$style.switchText">{{ item.text }}</span>
+ <div :class="$style.item_content">
+ <span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
+ </div>
</button>
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
- <span style="pointer-events: none;">{{ item.text }}</span>
- <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
+ <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span>
+ </div>
</button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
- <span>{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <div :class="$style.item_content">
+ <span :class="$style.item_content_text">{{ item.text }}</span>
+ <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ </div>
</button>
</template>
<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
@@ -62,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
@@ -90,19 +102,19 @@ const emit = defineEmits<{
(ev: 'hide'): void;
}>();
-let itemsEl = $shallowRef<HTMLDivElement>();
+const itemsEl = shallowRef<HTMLDivElement>();
-let items2: InnerMenuItem[] = $ref([]);
+const items2 = ref<InnerMenuItem[]>([]);
-let child = $shallowRef<InstanceType<typeof XChild>>();
+const child = shallowRef<InstanceType<typeof XChild>>();
-let keymap = $computed(() => ({
+const keymap = computed(() => ({
'up|k|shift+tab': focusUp,
'down|j|tab': focusDown,
'esc': close,
}));
-let childShowingItem = $ref<MenuItem | null>();
+const childShowingItem = ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer;
@@ -115,22 +127,22 @@ watch(() => props.items, () => {
if (item && 'then' in item) { // if item is Promise
items[i] = { type: 'pending' };
item.then(actualItem => {
- items2[i] = actualItem;
+ items2.value[i] = actualItem;
});
}
}
- items2 = items as InnerMenuItem[];
+ items2.value = items as InnerMenuItem[];
}, {
immediate: true,
});
const childMenu = ref<MenuItem[] | null>();
-let childTarget = $shallowRef<HTMLElement | null>();
+const childTarget = shallowRef<HTMLElement | null>();
function closeChild() {
childMenu.value = null;
- childShowingItem = null;
+ childShowingItem.value = null;
}
function childActioned() {
@@ -139,8 +151,8 @@ function childActioned() {
}
const onGlobalMousedown = (event: MouseEvent) => {
- if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
- if (child && child.checkHit(event)) return;
+ if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
+ if (child.value && child.value.checkHit(event)) return;
closeChild();
};
@@ -177,10 +189,10 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
});
emit('hide');
} else {
- childTarget = ev.currentTarget ?? ev.target;
+ childTarget.value = ev.currentTarget ?? ev.target;
// これでもリアクティビティは保たれる
childMenu.value = children;
- childShowingItem = item;
+ childShowingItem.value = item;
}
}
@@ -202,14 +214,14 @@ function focusDown() {
}
function switchItem(item: MenuSwitch & { ref: any }) {
- if (item.disabled) return;
+ if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return;
item.ref = !item.ref;
}
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
- if (itemsEl) focusNext(itemsEl.children[0], true, false);
+ if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
});
}
@@ -228,6 +240,7 @@ onBeforeUnmount(() => {
.root {
padding: 8px 0;
box-sizing: border-box;
+ max-width: 100vw;
min-width: 200px;
overflow: auto;
overscroll-behavior: contain;
@@ -267,7 +280,8 @@ onBeforeUnmount(() => {
}
.item {
- display: block;
+ display: flex;
+ align-items: center;
position: relative;
padding: 5px 16px;
width: 100%;
@@ -340,10 +354,6 @@ onBeforeUnmount(() => {
pointer-events: none;
font-size: 0.7em;
padding-bottom: 4px;
-
- > span {
- opacity: 0.7;
- }
}
&.pending {
@@ -373,6 +383,22 @@ onBeforeUnmount(() => {
}
}
+.item_content {
+ width: 100%;
+ max-width: 100vw;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ text-overflow: ellipsis;
+}
+
+.item_content_text {
+ max-width: calc(100vw - 4rem);
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
.switch {
position: relative;
display: flex;
@@ -406,6 +432,7 @@ onBeforeUnmount(() => {
.icon {
margin-right: 8px;
+ line-height: 1;
}
.caret {
@@ -419,9 +446,8 @@ onBeforeUnmount(() => {
}
.indicator {
- position: absolute;
- top: 5px;
- left: 13px;
+ display: flex;
+ align-items: center;
color: var(--indicator);
font-size: 12px;
animation: blink 1s infinite;
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 8d2a147306..f0a2c232bd 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import tinycolor from 'tinycolor2';
import { useInterval } from '@/scripts/use-interval.js';
@@ -43,11 +43,11 @@ const props = defineProps<{
const viewBoxX = 50;
const viewBoxY = 50;
const gradientId = uuid();
-let polylinePoints = $ref('');
-let polygonPoints = $ref('');
-let headX = $ref<number | null>(null);
-let headY = $ref<number | null>(null);
-let clock = $ref<number | null>(null);
+const polylinePoints = ref('');
+const polygonPoints = ref('');
+const headX = ref<number | null>(null);
+const headY = ref<number | null>(null);
+const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toRgbString();
@@ -60,12 +60,12 @@ function draw(): void {
(1 - (n / peak)) * viewBoxY,
]);
- polylinePoints = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
+ polylinePoints.value = _polylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
- polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
+ polygonPoints.value = `0,${ viewBoxY } ${ polylinePoints.value } ${ viewBoxX },${ viewBoxY }`;
- headX = _polylinePoints.at(-1)![0];
- headY = _polylinePoints.at(-1)![1];
+ headX.value = _polylinePoints.at(-1)![0];
+ headY.value = _polylinePoints.at(-1)![1];
}
watch(() => props.src, draw, { immediate: true });
diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/MkModPlayer.vue
index c24eaab2fa..055522d466 100644
--- a/packages/frontend/src/components/MkModPlayer.vue
+++ b/packages/frontend/src/components/MkModPlayer.vue
@@ -29,7 +29,7 @@
</template>
<script lang="ts" setup>
-import { ref, nextTick } from 'vue';
+import { ref, nextTick, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -71,9 +71,9 @@ const props = defineProps<{
module: Misskey.entities.DriveFile
}>();
-const isSensitive = $computed(() => { return props.module.isSensitive; });
-const url = $computed(() => { return props.module.url; });
-let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive && (defaultStore.state.nsfw !== 'ignore'));
+const isSensitive = computed(() => { return props.module.isSensitive; });
+const url = computed(() => { return props.module.url; });
+let hide = ref((defaultStore.state.nsfw === 'force') ? true : isSensitive.value && (defaultStore.state.nsfw !== 'ignore'));
let playing = ref(false);
let displayCanvas = ref<HTMLCanvasElement>();
let progress = ref<HTMLProgressElement>();
@@ -84,7 +84,7 @@ const rowBuffer = 24;
let buffer = null;
let isSeeking = false;
-player.value.load(url).then((result) => {
+player.value.load(url.value).then((result) => {
buffer = result;
try {
player.value.play(buffer);
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index ec5039c504..5cd31cdf7c 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch } from 'vue';
+import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
import { defaultStore } from '@/store.js';
@@ -89,14 +89,14 @@ const emit = defineEmits<{
provide('modal', true);
-let maxHeight = $ref<number>();
-let fixed = $ref(false);
-let transformOrigin = $ref('center');
-let showing = $ref(true);
-let content = $shallowRef<HTMLElement>();
+const maxHeight = ref<number>();
+const fixed = ref(false);
+const transformOrigin = ref('center');
+const showing = ref(true);
+const content = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex(props.zPriority);
-let useSendAnime = $ref(false);
-const type = $computed<ModalTypes>(() => {
+const useSendAnime = ref(false);
+const type = computed<ModalTypes>(() => {
if (props.preferType === 'auto') {
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
return 'drawer';
@@ -107,26 +107,26 @@ const type = $computed<ModalTypes>(() => {
return props.preferType!;
}
});
-const isEnableBgTransparent = $computed(() => props.transparentBg && (type === 'popup'));
-let transitionName = $computed((() =>
+const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup'));
+const transitionName = computed((() =>
defaultStore.state.animation
- ? useSendAnime
+ ? useSendAnime.value
? 'send'
- : type === 'drawer'
+ : type.value === 'drawer'
? 'modal-drawer'
- : type === 'popup'
+ : type.value === 'popup'
? 'modal-popup'
: 'modal'
: ''
));
-let transitionDuration = $computed((() =>
- transitionName === 'send'
+const transitionDuration = computed((() =>
+ transitionName.value === 'send'
? 400
- : transitionName === 'modal-popup'
+ : transitionName.value === 'modal-popup'
? 100
- : transitionName === 'modal'
+ : transitionName.value === 'modal'
? 200
- : transitionName === 'modal-drawer'
+ : transitionName.value === 'modal-drawer'
? 200
: 0
));
@@ -135,12 +135,12 @@ let contentClicking = false;
function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) {
- useSendAnime = true;
+ useSendAnime.value = true;
}
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
- showing = false;
+ showing.value = false;
emit('close');
}
@@ -149,8 +149,8 @@ function onBgClick() {
emit('click');
}
-if (type === 'drawer') {
- maxHeight = window.innerHeight / 1.5;
+if (type.value === 'drawer') {
+ maxHeight.value = window.innerHeight / 1.5;
}
const keymap = {
@@ -162,21 +162,21 @@ const SCROLLBAR_THICKNESS = 16;
const align = () => {
if (props.src == null) return;
- if (type === 'drawer') return;
- if (type === 'dialog') return;
+ if (type.value === 'drawer') return;
+ if (type.value === 'dialog') return;
- if (content == null) return;
+ if (content.value == null) return;
const srcRect = props.src.getBoundingClientRect();
- const width = content!.offsetWidth;
- const height = content!.offsetHeight;
+ const width = content.value!.offsetWidth;
+ const height = content.value!.offsetHeight;
let left;
let top;
- const x = srcRect.left + (fixed ? 0 : window.pageXOffset);
- const y = srcRect.top + (fixed ? 0 : window.pageYOffset);
+ const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset);
+ const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset);
if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2);
@@ -194,7 +194,7 @@ const align = () => {
top = y + props.src.offsetHeight;
}
- if (fixed) {
+ if (fixed.value) {
// 画面から横にはみ出る場合
if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width;
@@ -207,16 +207,16 @@ const align = () => {
if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
} else {
- maxHeight = upperSpace;
+ maxHeight.value = upperSpace;
top = (upperSpace + MARGIN) - height;
}
} else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height;
}
} else {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
}
} else {
// 画面から横にはみ出る場合
@@ -231,16 +231,16 @@ const align = () => {
if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
} else {
- maxHeight = upperSpace;
+ maxHeight.value = upperSpace;
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
}
} else {
- maxHeight = underSpace;
+ maxHeight.value = underSpace;
}
}
@@ -255,29 +255,29 @@ const align = () => {
let transformOriginX = 'center';
let transformOriginY = 'center';
- if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) {
+ if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'top';
- } else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) {
+ } else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) {
transformOriginY = 'bottom';
}
- if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) {
+ if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'left';
- } else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) {
+ } else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) {
transformOriginX = 'right';
}
- transformOrigin = `${transformOriginX} ${transformOriginY}`;
+ transformOrigin.value = `${transformOriginX} ${transformOriginY}`;
- content.style.left = left + 'px';
- content.style.top = top + 'px';
+ content.value.style.left = left + 'px';
+ content.value.style.top = top + 'px';
};
const onOpened = () => {
emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
- const el = content!.children[0];
+ const el = content.value!.children[0];
el.addEventListener('mousedown', ev => {
contentClicking = true;
window.addEventListener('mouseup', ev => {
@@ -299,7 +299,7 @@ onMounted(() => {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
}
- fixed = (type === 'drawer') || (getFixedContainer(props.src) != null);
+ fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick();
@@ -307,7 +307,7 @@ onMounted(() => {
}, { immediate: true });
nextTick(() => {
- alignObserver.observe(content!);
+ alignObserver.observe(content.value!);
});
});
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 800950ea82..b91988304d 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import MkModal from './MkModal.vue';
const props = withDefaults(defineProps<{
@@ -44,14 +44,14 @@ const emit = defineEmits<{
(event: 'ok'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
-let rootEl = $shallowRef<HTMLElement>();
-let headerEl = $shallowRef<HTMLElement>();
-let bodyWidth = $ref(0);
-let bodyHeight = $ref(0);
+const modal = shallowRef<InstanceType<typeof MkModal>>();
+const rootEl = shallowRef<HTMLElement>();
+const headerEl = shallowRef<HTMLElement>();
+const bodyWidth = ref(0);
+const bodyHeight = ref(0);
const close = () => {
- modal.close();
+ modal.value.close();
};
const onBgClick = () => {
@@ -67,14 +67,14 @@ const onKeydown = (evt) => {
};
const ro = new ResizeObserver((entries, observer) => {
- bodyWidth = rootEl.offsetWidth;
- bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
+ bodyWidth.value = rootEl.value.offsetWidth;
+ bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
});
onMounted(() => {
- bodyWidth = rootEl.offsetWidth;
- bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
- ro.observe(rootEl);
+ bodyWidth.value = rootEl.value.offsetWidth;
+ bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
+ ro.observe(rootEl.value);
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 74edc8903e..9ecf21071d 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted"
+ v-if="!hardMuted && !muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -50,14 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
- <MkNoteHeader :note="appearNote" :mini="true" v-on:click.stop/>
+ <MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
+ <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
@@ -79,31 +79,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
</div>
<div v-if="appearNote.files.length > 0">
- <MkMediaList :mediaList="appearNote.files" v-on:click.stop/>
+ <MkMediaList :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop />
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/>
+ <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
- <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false">
+ <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
- <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true">
+ <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
- <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction">
+ <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
- <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()">
+ <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
</button>
@@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--accent) !important;' : ''"
- v-on:click.stop
+ @click.stop
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
@@ -127,19 +127,19 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
:class="$style.footerButton"
class="_button"
- v-on:click.stop
+ @click.stop
@mousedown="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()">
+ <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)">
+ <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
@@ -152,7 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -161,10 +161,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</I18n>
</div>
+<div v-else>
+ <!--
+ MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
+ so MkNote create empty div instead of no elements
+ -->
+</div>
</template>
<script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
+import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue';
@@ -183,6 +189,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -206,6 +213,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
+ withHardMute?: boolean;
}>(), {
mock: false,
});
@@ -222,7 +230,7 @@ const router = useRouter();
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
function noteclick(id: string) {
const selection = document.getSelection();
@@ -234,7 +242,7 @@ function noteclick(id: string) {
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -246,15 +254,16 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.cw == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -266,27 +275,37 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
-const isMyRenote = $i && ($i.id === note.userId);
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
-const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const isLong = shouldCollapsed(appearNote, urls ?? []);
-const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null);
+const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
+const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
+const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
const translation = ref<any>(null);
const translating = ref(false);
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
-let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+
+function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+ if (mutedWords == null) return false;
+
+ if (checkWordMute(note, $i, mutedWords)) return true;
+ if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
+ if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ return false;
+}
const keymap = {
'r': () => reply(true),
@@ -301,20 +320,20 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
if (props.mock) {
watch(() => props.note, (to) => {
- note = deepClone(to);
+ note.value = deepClone(to);
}, { deep: true });
} else {
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
}
@@ -322,7 +341,7 @@ if (props.mock) {
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -333,14 +352,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -352,14 +371,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -419,7 +438,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -430,14 +449,14 @@ function renote(visibility: Visibility | 'local') {
if (!props.mock) {
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -449,16 +468,16 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
if (!props.mock) {
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -474,13 +493,13 @@ function quote() {
return;
}
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -499,10 +518,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -528,8 +547,8 @@ function reply(viaKeyboard = false): void {
return;
}
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -543,7 +562,7 @@ function like(): void {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -558,13 +577,15 @@ function like(): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
+ sound.play('reaction');
+
if (props.mock) {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -577,16 +598,18 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
if (props.mock) {
emit('reaction', reaction);
return;
}
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -613,8 +636,8 @@ function undoRenote(note) : void {
if (props.mock) {
return;
}
- os.api("notes/unrenote", {
- noteId: note.id
+ os.api('notes/unrenote', {
+ noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -648,7 +671,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
@@ -658,14 +681,14 @@ function menu(viaKeyboard = false): void {
return;
}
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -676,7 +699,7 @@ async function clip() {
return;
}
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -691,7 +714,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -701,17 +724,17 @@ function showRenoteMenu(viaKeyboard = false): void {
if (isMyRenote) {
pleaseLogin();
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
getUnrenote(),
], renoteTime.value, {
viaKeyboard: viaKeyboard,
});
} else {
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
- getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
+ getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
viaKeyboard: viaKeyboard,
@@ -749,7 +772,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
});
isDeleted.value = true;
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 93e39ff033..f29b9db6ae 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -93,8 +93,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
@@ -237,6 +237,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -248,12 +249,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { MenuItem } from '@/types/menu.js';
import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -264,12 +264,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -281,15 +281,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -301,26 +301,25 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
-
-const isMyRenote = $i && ($i.id === note.userId);
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref(null);
const translating = ref(false);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
+const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
+const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
const quotes = ref<Misskey.entities.Note[]>([]);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
watch(() => props.expandAllCws, (expandAllCws) => {
@@ -328,8 +327,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -348,41 +347,41 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
-let tab = $ref('replies');
-let reactionTabType = $ref(null);
+const tab = ref('replies');
+const reactionTabType = ref(null);
-const renotesPagination = $computed(() => ({
+const renotesPagination = computed(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
},
}));
-const reactionsPagination = $computed(() => ({
+const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
- noteId: appearNote.id,
- type: reactionTabType,
+ noteId: appearNote.value.id,
+ type: reactionTabType.value,
},
}));
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -393,14 +392,14 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -412,7 +411,7 @@ useTooltip(quoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
@@ -467,7 +466,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -477,13 +476,13 @@ function renote(visibility: Visibility | 'local') {
}
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -495,15 +494,15 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -515,13 +514,13 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -540,10 +539,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -567,8 +566,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -578,9 +577,9 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -593,11 +592,13 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -610,7 +611,7 @@ function like(): void {
pleaseLogin();
showMovedDialog();
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -632,8 +633,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -661,27 +662,27 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function clip() {
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -693,7 +694,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -715,7 +716,7 @@ const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
os.api('notes/children', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
showQuotes: false,
}).then(res => {
@@ -730,7 +731,7 @@ const quotesLoaded = ref(false);
function loadQuotes() {
quotesLoaded.value = true;
os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
quote: true,
}).then(res => {
@@ -745,13 +746,13 @@ const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
os.api('notes/conversation', {
- noteId: appearNote.replyId,
+ noteId: appearNote.value.replyId,
}).then(res => {
conversation.value = res.reverse();
});
}
-if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
+if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
function animatedMFM() {
if (allowAnim.value) {
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index 552f8137ed..c517bc6800 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -11,7 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="true"/>
</div>
<div>
- <div>
+ <p v-if="useCw" :class="$style.cw">
+ <Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
+ <MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
+ </p>
+ <div v-show="!useCw || showContent">
<Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/>
</div>
</div>
@@ -20,11 +24,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
+import MkCwButton from '@/components/MkCwButton.vue';
+
+const showContent = ref(false);
const props = defineProps<{
text: string;
+ files: Misskey.entities.DriveFile[];
+ poll?: {
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string | null;
+ expiredAfter: string | null;
+ };
+ useCw: boolean;
+ cw: string | null;
user: Misskey.entities.User;
}>();
</script>
@@ -53,6 +69,14 @@ const props = defineProps<{
min-width: 0;
}
+.cw {
+ cursor: default;
+ display: block;
+ margin: 0;
+ padding: 0;
+ overflow-wrap: break-word;
+}
+
.header {
margin-bottom: 2px;
font-weight: bold;
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index bc0f82d44d..7a6109ee0b 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
- <MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
@@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
-import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@@ -36,10 +35,10 @@ const props = defineProps<{
hideFiles?: boolean;
}>();
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
</script>
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 5b1e1af308..8d394c0c15 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.content">
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="note"/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/>
@@ -93,15 +93,14 @@ import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { userPage } from "@/filters/user.js";
-import { checkWordMute } from "@/scripts/check-word-mute.js";
-import { defaultStore } from "@/store.js";
+import { userPage } from '@/filters/user.js';
+import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import type { MenuItem } from '@/types/menu.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -131,7 +130,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
+let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const isRenote = (
@@ -143,13 +142,13 @@ const isRenote = (
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
+ note: appearNote,
isDeletedRef: isDeleted,
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -230,8 +229,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -245,13 +244,13 @@ function undoRenote() : void {
}
}
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-let replies: Misskey.entities.Note[] = $ref([]);
+let replies = ref<Misskey.entities.Note[]>([]);
function boostVisibility() {
os.popupMenu([
@@ -293,7 +292,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -333,12 +332,12 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -358,9 +357,9 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -394,7 +393,7 @@ if (props.detail) {
limit: numberOfReplies.value,
showQuotes: false,
}).then(res => {
- replies = res;
+ replies.value = res;
});
}
</script>
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 0d2f0020d1..fc1c8a0f09 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ad="true"
:class="$style.notes"
>
- <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+ <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
</MkDateSeparatedList>
<MkDateSeparatedList
v-else-if="defaultStore.state.noteDesign === 'sharkey'"
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index ae5be0f2d4..2901139220 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
+ <MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
@@ -36,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i>
+ <img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/>
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
@@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</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 === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@@ -86,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
</MkA>
+ <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
+ {{ notification.role.name }}
+ </div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
@@ -130,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, shallowRef } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 3d5a56975b..6725776f43 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref, Ref } from 'vue';
+import { ref, Ref, shallowRef } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
@@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{
excludeTypes: () => [],
});
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
@@ -61,7 +61,7 @@ function ok() {
.filter(type => !typesMap[type].value),
});
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function disableAll() {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index bfe668a165..a157820d56 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }">
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+ <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
<MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+ <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
@@ -29,13 +29,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import SkNote from '@/components/SkNote.vue';
import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js';
@@ -48,7 +47,7 @@ const props = defineProps<{
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
-const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
+const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
endpoint: 'i/notifications-grouped' as const,
limit: 20,
params: computed(() => ({
@@ -60,7 +59,7 @@ const pagination: Paging = defaultStore.state.useGroupedNotifications ? {
params: computed(() => ({
excludeTypes: props.excludeTypes ?? undefined,
})),
-};
+});
function onNotification(notification) {
const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index ac957d93dc..702bb95dc7 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, shallowRef, ref } from 'vue';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
@@ -22,13 +22,13 @@ const props = withDefaults(defineProps<{
maxHeight: 200,
});
-let content = $shallowRef<HTMLElement>();
-let omitted = $ref(false);
-let ignoreOmit = $ref(false);
+const content = shallowRef<HTMLElement>();
+const omitted = ref(false);
+const ignoreOmit = ref(false);
const calcOmit = () => {
- if (omitted || ignoreOmit) return;
- omitted = content.offsetHeight > props.maxHeight;
+ if (omitted.value || ignoreOmit.value) return;
+ omitted.value = content.value.offsetHeight > props.maxHeight;
};
const omitObserver = new ResizeObserver((entries, observer) => {
@@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
onMounted(() => {
calcOmit();
- omitObserver.observe(content);
+ omitObserver.observe(content.value);
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 05b577c49c..6c8a0e56a6 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -114,7 +114,6 @@ const props = defineProps<{
& + article {
left: 0;
- width: 100%;
}
}
}
@@ -124,6 +123,7 @@ const props = defineProps<{
> .thumbnail {
height: 80px;
+ overflow: clip;
}
> article {
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 163cba5e3c..d1d4c2106c 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, onMounted, onUnmounted, provide, shallowRef } from 'vue';
+import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
@@ -55,16 +55,16 @@ defineEmits<{
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
const contents = shallowRef<HTMLElement>();
-let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
-let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
-const history = $ref<{ path: string; key: any; }[]>([{
+const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
+const history = ref<{ path: string; key: any; }[]>([{
path: router.getCurrentPath(),
key: router.getCurrentKey(),
}]);
-const buttonsLeft = $computed(() => {
+const buttonsLeft = computed(() => {
const buttons = [];
- if (history.length > 1) {
+ if (history.value.length > 1) {
buttons.push({
icon: 'ph-arrow-left ph-bold ph-lg',
onClick: back,
@@ -73,7 +73,7 @@ const buttonsLeft = $computed(() => {
return buttons;
});
-const buttonsRight = $computed(() => {
+const buttonsRight = computed(() => {
const buttons = [{
icon: 'ph-arrow-clockwise ph-bold ph-lg',
title: i18n.ts.reload,
@@ -86,22 +86,22 @@ const buttonsRight = $computed(() => {
return buttons;
});
-let reloadCount = $ref(0);
+const reloadCount = ref(0);
router.addListener('push', ctx => {
- history.push({ path: ctx.path, key: ctx.key });
+ history.value.push({ path: ctx.path, key: ctx.key });
});
provide('router', router);
provideMetadataReceiver((info) => {
- pageMetadata = info;
+ pageMetadata.value = info;
});
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
provide('forceSpacerMin', true);
provide('shouldBackButton', false);
-const contextmenu = $computed(() => ([{
+const contextmenu = computed(() => ([{
icon: 'ph-eject ph-bold ph-lg',
text: i18n.ts.showInPage,
action: expand,
@@ -113,8 +113,8 @@ const contextmenu = $computed(() => ([{
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
- window.open(url + router.getCurrentPath(), '_blank');
- windowEl.close();
+ window.open(url + router.getCurrentPath(), '_blank', 'noopener');
+ windowEl.value.close();
},
}, {
icon: 'ph-link ph-bold ph-lg',
@@ -125,26 +125,26 @@ const contextmenu = $computed(() => ([{
}]));
function back() {
- history.pop();
- router.replace(history.at(-1)!.path, history.at(-1)!.key);
+ history.value.pop();
+ router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
}
function reload() {
- reloadCount++;
+ reloadCount.value++;
}
function close() {
- windowEl.close();
+ windowEl.value.close();
}
function expand() {
mainRouter.push(router.getCurrentPath(), 'forcePage');
- windowEl.close();
+ windowEl.value.close();
}
function popout() {
- _popout(router.getCurrentPath(), windowEl.$el);
- windowEl.close();
+ _popout(router.getCurrentPath(), windowEl.value.$el);
+ windowEl.value.close();
}
useScrollPositionManager(() => getScrollContainer(contents.value), router);
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index e7796dfcb5..07347eda29 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
-import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue';
+import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
@@ -105,12 +105,12 @@ const emit = defineEmits<{
(ev: 'status', error: boolean): void;
}>();
-let rootEl = $shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
// 遡り中かどうか
-let backed = $ref(false);
+const backed = ref(false);
-let scrollRemove = $ref<(() => void) | null>(null);
+const scrollRemove = ref<(() => void) | null>(null);
/**
* 表示するアイテムのソース
@@ -142,8 +142,8 @@ const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
-const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
-const scrollableElement = $computed(() => contentEl ? getScrollContainer(contentEl) : document.body);
+const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
+const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
const visibility = useDocumentVisibility();
@@ -153,40 +153,40 @@ const BACKGROUND_PAUSE_WAIT_SEC = 10;
// 先頭が表示されているかどうかを検出
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
-let scrollObserver = $ref<IntersectionObserver>();
+const scrollObserver = ref<IntersectionObserver>();
-watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
- if (scrollObserver) scrollObserver.disconnect();
+watch([() => props.pagination.reversed, scrollableElement], () => {
+ if (scrollObserver.value) scrollObserver.value.disconnect();
- scrollObserver = new IntersectionObserver(entries => {
- backed = entries[0].isIntersecting;
+ scrollObserver.value = new IntersectionObserver(entries => {
+ backed.value = entries[0].isIntersecting;
}, {
- root: scrollableElement,
+ root: scrollableElement.value,
rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px',
threshold: 0.01,
});
}, { immediate: true });
-watch($$(rootEl), () => {
- scrollObserver?.disconnect();
+watch(rootEl, () => {
+ scrollObserver.value?.disconnect();
nextTick(() => {
- if (rootEl) scrollObserver?.observe(rootEl);
+ if (rootEl.value) scrollObserver.value?.observe(rootEl.value);
});
});
-watch([$$(backed), $$(contentEl)], () => {
- if (!backed) {
- if (!contentEl) return;
+watch([backed, contentEl], () => {
+ if (!backed.value) {
+ if (!contentEl.value) return;
- scrollRemove = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl, executeQueue, TOLERANCE);
+ scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE);
} else {
- if (scrollRemove) scrollRemove();
- scrollRemove = null;
+ if (scrollRemove.value) scrollRemove.value();
+ scrollRemove.value = null;
}
});
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
-watch(() => props.pagination.params, init, { deep: true });
+watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true });
watch(queue, (a, b) => {
if (a.size === 0 && b.size === 0) return;
@@ -206,6 +206,7 @@ async function init(): Promise<void> {
await os.api(props.pagination.endpoint, {
...params,
limit: props.pagination.limit ?? 10,
+ allowPartial: true,
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
@@ -253,14 +254,14 @@ const fetchMore = async (): Promise<void> => {
}
const reverseConcat = _res => {
- const oldHeight = scrollableElement ? scrollableElement.scrollHeight : getBodyScrollHeight();
- const oldScroll = scrollableElement ? scrollableElement.scrollTop : window.scrollY;
+ const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
+ const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
items.value = concatMapWithArray(items.value, _res);
return nextTick(() => {
- if (scrollableElement) {
- scroll(scrollableElement, { top: oldScroll + (scrollableElement.scrollHeight - oldHeight), behavior: 'instant' });
+ if (scrollableElement.value) {
+ scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}
@@ -350,7 +351,7 @@ const appearFetchMoreAhead = async (): Promise<void> => {
fetchMoreAppearTimeout();
};
-const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl!, TOLERANCE);
+const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE);
watch(visibility, () => {
if (visibility.value === 'hidden') {
@@ -444,11 +445,11 @@ onActivated(() => {
});
onDeactivated(() => {
- isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl ? rootEl.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
+ isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0;
});
function toBottom() {
- scrollToBottom(contentEl!);
+ scrollToBottom(contentEl.value!);
}
onBeforeMount(() => {
@@ -476,13 +477,13 @@ onBeforeUnmount(() => {
clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
- scrollObserver?.disconnect();
+ scrollObserver.value?.disconnect();
});
defineExpose({
items,
queue,
- backed,
+ backed: backed.value,
more,
reload,
prepend,
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 3f244c42fd..711c54c7f1 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -49,22 +49,22 @@ const emit = defineEmits<{
(ev: 'cancelled'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-const passwordInput = $shallowRef<InstanceType<typeof MkInput>>();
-const password = $ref('');
-const token = $ref(null);
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
+const password = ref('');
+const token = ref(null);
function onClose() {
emit('cancelled');
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function done(res) {
- emit('done', { password, token });
- if (dialog) dialog.close();
+ emit('done', { password: password.value, token: token.value });
+ if (dialog.value) dialog.value.close();
}
onMounted(() => {
- if (passwordInput) passwordInput.focus();
+ if (passwordInput.value) passwordInput.value.focus();
});
</script>
diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue
index 0bc98f4334..a741a3f7a8 100644
--- a/packages/frontend/src/components/MkPlusOneEffect.vue
+++ b/packages/frontend/src/components/MkPlusOneEffect.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
const props = withDefaults(defineProps<{
@@ -23,13 +23,13 @@ const emit = defineEmits<{
(ev: 'end'): void;
}>();
-let up = $ref(false);
+const up = ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (45 - (Math.random() * 90)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
- up = true;
+ up.value = true;
}, 10);
window.setTimeout(() => {
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 146b9d7ccf..1d92374f4f 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -10,10 +10,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, shallowRef } from 'vue';
import MkModal from './MkModal.vue';
import MkMenu from './MkMenu.vue';
-import { MenuItem } from '@/types/menu';
+import { MenuItem } from '@/types/menu.js';
defineProps<{
items: MenuItem[];
@@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: 'closing'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const manualShowing = ref(true);
const hiding = ref(false);
@@ -60,14 +60,14 @@ function hide() {
hiding.value = true;
// closeは呼ぶ必要がある
- modal?.close();
+ modal.value?.close();
}
function close() {
manualShowing.value = false;
// closeは呼ぶ必要がある
- modal?.close();
+ modal.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5c9ac40427..c9784fc40f 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -67,13 +67,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
- <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+ <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
+ <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
</div>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
<XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/>
<MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
- <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :user="postAccount ?? $i"/>
+ <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/>
<div v-if="showingOptions" style="padding: 8px 16px;">
</div>
<footer :class="$style.footer">
@@ -99,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue';
+import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -125,6 +126,7 @@ import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
+import { emojiPicker } from '@/scripts/emoji-picker.js';
const modal = inject('modal');
@@ -135,6 +137,7 @@ const props = withDefaults(defineProps<{
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
+ initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
@@ -144,7 +147,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
- editId?: Misskey.entities.Note["id"];
+ editId?: Misskey.entities.Note['id'];
mock?: boolean;
}>(), {
initialVisibleUsers: () => [],
@@ -163,41 +166,42 @@ const emit = defineEmits<{
(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
}>();
-const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
-const cwInputEl = $shallowRef<HTMLInputElement | null>(null);
-const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null);
-const visibilityButton = $shallowRef<HTMLElement | null>(null);
+const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
+const cwInputEl = shallowRef<HTMLInputElement | null>(null);
+const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
+const visibilityButton = shallowRef<HTMLElement | null>(null);
-let posting = $ref(false);
-let posted = $ref(false);
-let text = $ref(props.initialText ?? '');
-let files = $ref(props.initialFiles ?? []);
-let poll = $ref<{
+const posting = ref(false);
+const posted = ref(false);
+const text = ref(props.initialText ?? '');
+const files = ref(props.initialFiles ?? []);
+const poll = ref<{
choices: string[];
multiple: boolean;
expiresAt: string | null;
expiredAfter: string | null;
} | null>(null);
-let useCw = $ref(false);
-let showPreview = $ref(defaultStore.state.showPreview);
-watch($$(showPreview), () => defaultStore.set('showPreview', showPreview));
-let cw = $ref<string | null>(null);
-let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
-let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
-let visibleUsers = $ref([]);
+const useCw = ref<boolean>(!!props.initialCw);
+const showPreview = ref(defaultStore.state.showPreview);
+watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
+const cw = ref<string | null>(props.initialCw ?? null);
+const localOnly = ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
+const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof Misskey.noteVisibilities[number]);
+const visibleUsers = ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
-let reactionAcceptance = $ref(defaultStore.state.reactionAcceptance);
-let autocomplete = $ref(null);
-let draghover = $ref(false);
-let quoteId = $ref(null);
-let hasNotSpecifiedMentions = $ref(false);
-let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
-let imeText = $ref('');
-let showingOptions = $ref(false);
+const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
+const autocomplete = ref(null);
+const draghover = ref(false);
+const quoteId = ref(null);
+const hasNotSpecifiedMentions = ref(false);
+const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
+const imeText = ref('');
+const showingOptions = ref(false);
+const textAreaReadOnly = ref(false);
-const draftKey = $computed((): string => {
+const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
if (props.renote) {
@@ -211,7 +215,7 @@ const draftKey = $computed((): string => {
return key;
});
-const placeholder = $computed((): string => {
+const placeholder = computed((): string => {
if (props.renote) {
return i18n.ts._postForm.quotePlaceholder;
} else if (props.reply) {
@@ -231,7 +235,7 @@ const placeholder = $computed((): string => {
}
});
-const submitText = $computed((): string => {
+const submitText = computed((): string => {
return props.renote
? i18n.ts.quote
: props.reply
@@ -239,45 +243,45 @@ const submitText = $computed((): string => {
: i18n.ts.note;
});
-const textLength = $computed((): number => {
- return (text + imeText).trim().length;
+const textLength = computed((): number => {
+ return (text.value + imeText.value).trim().length;
});
-const maxTextLength = $computed((): number => {
+const maxTextLength = computed((): number => {
return instance ? instance.maxNoteTextLength : 1000;
});
-const canPost = $computed((): boolean => {
- return !props.mock && !posting && !posted &&
- (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
- (textLength <= maxTextLength) &&
- (!poll || poll.choices.length >= 2);
+const canPost = computed((): boolean => {
+ return !props.mock && !posting.value && !posted.value &&
+ (1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
+ (textLength.value <= maxTextLength.value) &&
+ (!poll.value || poll.value.choices.length >= 2);
});
-const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
-const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags'));
+const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
+const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
-watch($$(text), () => {
+watch(text, () => {
checkMissingMention();
}, { immediate: true });
-watch($$(visibility), () => {
+watch(visibility, () => {
checkMissingMention();
}, { immediate: true });
-watch($$(visibleUsers), () => {
+watch(visibleUsers, () => {
checkMissingMention();
}, {
deep: true,
});
if (props.mention) {
- text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
- text += ' ';
+ text.value = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
+ text.value += ' ';
}
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
- text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
+ text.value = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
}
if (props.reply && props.reply.text != null) {
@@ -295,32 +299,32 @@ if (props.reply && props.reply.text != null) {
if ($i.username === x.username && (x.host == null || x.host === host)) continue;
// 重複は除外
- if (text.includes(`${mention} `)) continue;
+ if (text.value.includes(`${mention} `)) continue;
- text += `${mention} `;
+ text.value += `${mention} `;
}
}
-if ($i?.isSilenced && visibility === 'public') {
- visibility = 'home';
+if ($i?.isSilenced && visibility.value === 'public') {
+ visibility.value = 'home';
}
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
}
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
- if (props.reply.visibility === 'home' && visibility === 'followers') {
- visibility = 'followers';
- } else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') {
- visibility = 'specified';
+ if (props.reply.visibility === 'home' && visibility.value === 'followers') {
+ visibility.value = 'followers';
+ } else if (['home', 'followers'].includes(props.reply.visibility) && visibility.value === 'specified') {
+ visibility.value = 'specified';
} else {
- visibility = props.reply.visibility;
+ visibility.value = props.reply.visibility;
}
- if (visibility === 'specified') {
+ if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) {
os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
@@ -338,24 +342,24 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
}
if (props.specified) {
- visibility = 'specified';
+ visibility.value = 'specified';
pushVisibleUser(props.specified);
}
// keep cw when reply
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
- useCw = true;
- cw = props.reply.cw;
+ useCw.value = true;
+ cw.value = props.reply.cw;
}
function watchForDraft() {
- watch($$(text), () => saveDraft());
- watch($$(useCw), () => saveDraft());
- watch($$(cw), () => saveDraft());
- watch($$(poll), () => saveDraft());
- watch($$(files), () => saveDraft(), { deep: true });
- watch($$(visibility), () => saveDraft());
- watch($$(localOnly), () => saveDraft());
+ watch(text, () => saveDraft());
+ watch(useCw, () => saveDraft());
+ watch(cw, () => saveDraft());
+ watch(poll, () => saveDraft());
+ watch(files, () => saveDraft(), { deep: true });
+ watch(visibility, () => saveDraft());
+ watch(localOnly, () => saveDraft());
}
function MFMWindow() {
@@ -363,36 +367,36 @@ function MFMWindow() {
}
function checkMissingMention() {
- if (visibility === 'specified') {
- const ast = mfm.parse(text);
+ if (visibility.value === 'specified') {
+ const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
- if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
- hasNotSpecifiedMentions = true;
+ if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
+ hasNotSpecifiedMentions.value = true;
return;
}
}
- hasNotSpecifiedMentions = false;
}
+ hasNotSpecifiedMentions.value = false;
}
function addMissingMention() {
- const ast = mfm.parse(text);
+ const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
- if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
+ if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
os.api('users/show', { username: x.username, host: x.host }).then(user => {
- visibleUsers.push(user);
+ visibleUsers.value.push(user);
});
}
}
}
function togglePoll() {
- if (poll) {
- poll = null;
+ if (poll.value) {
+ poll.value = null;
} else {
- poll = {
+ poll.value = {
choices: ['', ''],
multiple: false,
expiresAt: null,
@@ -402,13 +406,13 @@ function togglePoll() {
}
function addTag(tag: string) {
- insertTextAtCursor(textareaEl, ` #${tag} `);
+ insertTextAtCursor(textareaEl.value, ` #${tag} `);
}
function focus() {
- if (textareaEl) {
- textareaEl.focus();
- textareaEl.setSelectionRange(textareaEl.value.length, textareaEl.value.length);
+ if (textareaEl.value) {
+ textareaEl.value.focus();
+ textareaEl.value.setSelectionRange(textareaEl.value.value.length, textareaEl.value.value.length);
}
}
@@ -417,55 +421,55 @@ function chooseFileFrom(ev) {
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
for (const file of files_) {
- files.push(file);
+ files.value.push(file);
}
});
}
function detachFile(id) {
- files = files.filter(x => x.id !== id);
+ files.value = files.value.filter(x => x.id !== id);
}
function updateFileSensitive(file, sensitive) {
if (props.mock) {
emit('fileChangeSensitive', file.id, sensitive);
}
- files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+ files.value[files.value.findIndex(x => x.id === file.id)].isSensitive = sensitive;
}
function updateFileName(file, name) {
- files[files.findIndex(x => x.id === file.id)].name = name;
+ files.value[files.value.findIndex(x => x.id === file.id)].name = name;
}
function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities.DriveFile): void {
- files[files.findIndex(x => x.id === file.id)] = newFile;
+ files.value[files.value.findIndex(x => x.id === file.id)] = newFile;
}
function upload(file: File, name?: string): void {
if (props.mock) return;
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
- files.push(res);
+ files.value.push(res);
});
}
function setVisibility() {
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return;
}
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
- currentVisibility: visibility,
+ currentVisibility: visibility.value,
isSilenced: $i?.isSilenced,
- localOnly: localOnly,
- src: visibilityButton,
+ localOnly: localOnly.value,
+ src: visibilityButton.value,
}, {
changeVisibility: v => {
- visibility = v;
+ visibility.value = v;
if (defaultStore.state.rememberNoteVisibility) {
- defaultStore.set('visibility', visibility);
+ defaultStore.set('visibility', visibility.value);
}
},
}, 'closed');
@@ -473,14 +477,14 @@ function setVisibility() {
async function toggleLocalOnly() {
if (props.channel) {
- visibility = 'public';
- localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+ visibility.value = 'public';
+ localOnly.value = true; // TODO: チャンネルが連合するようになった折には消す
return;
}
const neverShowInfo = miLocalStorage.getItem('neverShowLocalOnlyInfo');
- if (!localOnly && neverShowInfo !== 'true') {
+ if (!localOnly.value && neverShowInfo !== 'true') {
const confirm = await os.actions({
type: 'question',
title: i18n.ts.disableFederationConfirm,
@@ -510,7 +514,7 @@ async function toggleLocalOnly() {
}
}
- localOnly = !localOnly;
+ localOnly.value = !localOnly.value;
}
async function toggleReactionAcceptance() {
@@ -523,15 +527,15 @@ async function toggleReactionAcceptance() {
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
],
- default: reactionAcceptance,
+ default: reactionAcceptance.value,
});
if (select.canceled) return;
- reactionAcceptance = select.result;
+ reactionAcceptance.value = select.result;
}
function pushVisibleUser(user) {
- if (!visibleUsers.some(u => u.username === user.username && u.host === user.host)) {
- visibleUsers.push(user);
+ if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
+ visibleUsers.value.push(user);
}
}
@@ -539,34 +543,34 @@ function addVisibleUser() {
os.selectUser().then(user => {
pushVisibleUser(user);
- if (!text.toLowerCase().includes(`@${user.username.toLowerCase()}`)) {
- text = `@${Misskey.acct.toString(user)} ${text}`;
+ if (!text.value.toLowerCase().includes(`@${user.username.toLowerCase()}`)) {
+ text.value = `@${Misskey.acct.toString(user)} ${text.value}`;
}
});
}
function removeVisibleUser(user) {
- visibleUsers = erase(user, visibleUsers);
+ visibleUsers.value = erase(user, visibleUsers.value);
}
function clear() {
- text = '';
- files = [];
- poll = null;
- quoteId = null;
+ text.value = '';
+ files.value = [];
+ poll.value = null;
+ quoteId.value = null;
}
function onKeydown(ev: KeyboardEvent) {
- if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post();
+ if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post();
if (ev.key === 'Escape') emit('esc');
}
function onCompositionUpdate(ev: CompositionEvent) {
- imeText = ev.data;
+ imeText.value = ev.data;
}
function onCompositionEnd(ev: CompositionEvent) {
- imeText = '';
+ imeText.value = '';
}
async function onPaste(ev: ClipboardEvent) {
@@ -584,7 +588,7 @@ async function onPaste(ev: ClipboardEvent) {
const paste = ev.clipboardData.getData('text');
- if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
+ if (!props.renote && !quoteId.value && paste.startsWith(url + '/notes/')) {
ev.preventDefault();
os.confirm({
@@ -592,11 +596,11 @@ async function onPaste(ev: ClipboardEvent) {
text: i18n.ts.quoteQuestion,
}).then(({ canceled }) => {
if (canceled) {
- insertTextAtCursor(textareaEl, paste);
+ insertTextAtCursor(textareaEl.value, paste);
return;
}
- quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+ quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
});
}
}
@@ -607,7 +611,7 @@ function onDragover(ev) {
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
ev.preventDefault();
- draghover = true;
+ draghover.value = true;
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
@@ -628,15 +632,15 @@ function onDragover(ev) {
}
function onDragenter(ev) {
- draghover = true;
+ draghover.value = true;
}
function onDragleave(ev) {
- draghover = false;
+ draghover.value = false;
}
function onDrop(ev): void {
- draghover = false;
+ draghover.value = false;
// ファイルだったら
if (ev.dataTransfer.files.length > 0) {
@@ -649,7 +653,7 @@ function onDrop(ev): void {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
- files.push(file);
+ files.value.push(file);
ev.preventDefault();
}
//#endregion
@@ -660,16 +664,16 @@ function saveDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
- draftData[draftKey] = {
+ draftData[draftKey.value] = {
updatedAt: new Date(),
data: {
- text: text,
- useCw: useCw,
- cw: cw,
- visibility: visibility,
- localOnly: localOnly,
- files: files,
- poll: poll,
+ text: text.value,
+ useCw: useCw.value,
+ cw: cw.value,
+ visibility: visibility.value,
+ localOnly: localOnly.value,
+ files: files.value,
+ poll: poll.value,
},
};
@@ -679,13 +683,13 @@ function saveDraft() {
function deleteDraft() {
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
- delete draftData[draftKey];
+ delete draftData[draftKey.value];
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
}
async function post(ev?: MouseEvent) {
- if (useCw && (cw == null || cw.trim() === '')) {
+ if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({
type: 'error',
text: i18n.ts.cwNotationRequired,
@@ -704,13 +708,13 @@ async function post(ev?: MouseEvent) {
if (props.mock) return;
const annoying =
- text.includes('$[x2') ||
- text.includes('$[x3') ||
- text.includes('$[x4') ||
- text.includes('$[scale') ||
- text.includes('$[position');
+ text.value.includes('$[x2') ||
+ text.value.includes('$[x3') ||
+ text.value.includes('$[x4') ||
+ text.value.includes('$[scale') ||
+ text.value.includes('$[position');
- if (annoying && visibility === 'public') {
+ if (annoying && visibility.value === 'public') {
const { canceled, result } = await os.actions({
type: 'warning',
text: i18n.ts.thisPostMayBeAnnoying,
@@ -730,27 +734,27 @@ async function post(ev?: MouseEvent) {
if (canceled) return;
if (result === 'cancel') return;
if (result === 'home') {
- visibility = 'home';
+ visibility.value = 'home';
}
}
let postData = {
- text: text === '' ? null : text,
- fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
+ text: text.value === '' ? null : text.value,
+ fileIds: files.value.length > 0 ? files.value.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
- renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
+ renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined,
channelId: props.channel ? props.channel.id : undefined,
- poll: poll,
- cw: useCw ? cw ?? '' : null,
- localOnly: localOnly,
- visibility: visibility,
- visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
- reactionAcceptance,
+ poll: poll.value,
+ cw: useCw.value ? cw.value ?? '' : null,
+ localOnly: localOnly.value,
+ visibility: visibility.value,
+ visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined,
+ reactionAcceptance: reactionAcceptance.value,
editId: props.editId ? props.editId : undefined,
};
- if (withHashtags && hashtags && hashtags.trim() !== '') {
- const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+ if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
+ const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
}
@@ -767,15 +771,15 @@ async function post(ev?: MouseEvent) {
let token = undefined;
- if (postAccount) {
+ if (postAccount.value) {
const storedAccounts = await getAccounts();
- token = storedAccounts.find(x => x.id === postAccount.id)?.token;
+ token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
}
- posting = true;
- os.api(postData.editId ? "notes/edit" : "notes/create", postData, token).then(() => {
+ posting.value = true;
+ os.api(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
- posted = true;
+ posted.value = true;
} else {
clear();
}
@@ -787,8 +791,8 @@ async function post(ev?: MouseEvent) {
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
}
- posting = false;
- postAccount = null;
+ posting.value = false;
+ postAccount.value = null;
incNotesCount();
if (notesCount === 1) {
@@ -833,7 +837,7 @@ async function post(ev?: MouseEvent) {
}
});
}).catch(err => {
- posting = false;
+ posting.value = false;
os.alert({
type: 'error',
text: err.message + '\n' + (err as any).id,
@@ -847,12 +851,23 @@ function cancel() {
function insertMention() {
os.selectUser().then(user => {
- insertTextAtCursor(textareaEl, '@' + Misskey.acct.toString(user) + ' ');
+ insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
});
}
async function insertEmoji(ev: MouseEvent) {
- os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl);
+ textAreaReadOnly.value = true;
+
+ emojiPicker.show(
+ ev.currentTarget ?? ev.target,
+ emoji => {
+ insertTextAtCursor(textareaEl.value, emoji);
+ },
+ () => {
+ textAreaReadOnly.value = false;
+ nextTick(() => focus());
+ },
+ );
}
function showActions(ev) {
@@ -860,17 +875,17 @@ function showActions(ev) {
text: action.title,
action: () => {
action.handler({
- text: text,
- cw: cw,
+ text: text.value,
+ cw: cw.value,
}, (key, value) => {
- if (key === 'text') { text = value; }
- if (key === 'cw') { useCw = value !== null; cw = value; }
+ if (key === 'text') { text.value = value; }
+ if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
});
},
})), ev.currentTarget ?? ev.target);
}
-let postAccount = $ref<Misskey.entities.UserDetailed | null>(null);
+const postAccount = ref<Misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) {
if (props.mock) return;
@@ -878,12 +893,12 @@ function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
- active: postAccount != null ? postAccount.id : $i.id,
+ active: postAccount.value != null ? postAccount.value.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
- postAccount = null;
+ postAccount.value = null;
} else {
- postAccount = account;
+ postAccount.value = account;
}
},
}, ev);
@@ -899,23 +914,23 @@ onMounted(() => {
}
// TODO: detach when unmount
- new Autocomplete(textareaEl, $$(text));
- new Autocomplete(cwInputEl, $$(cw));
- new Autocomplete(hashtagsInputEl, $$(hashtags));
+ new Autocomplete(textareaEl.value, text);
+ new Autocomplete(cwInputEl.value, cw);
+ new Autocomplete(hashtagsInputEl.value, hashtags);
nextTick(() => {
// 書きかけの投稿を復元
if (!props.instant && !props.mention && !props.specified && !props.mock) {
- const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
+ const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey.value];
if (draft) {
- text = draft.data.text;
- useCw = draft.data.useCw;
- cw = draft.data.cw;
- visibility = draft.data.visibility;
- localOnly = draft.data.localOnly;
- files = (draft.data.files || []).filter(draftFile => draftFile);
+ text.value = draft.data.text;
+ useCw.value = draft.data.useCw;
+ cw.value = draft.data.cw;
+ visibility.value = draft.data.visibility;
+ localOnly.value = draft.data.localOnly;
+ files.value = (draft.data.files || []).filter(draftFile => draftFile);
if (draft.data.poll) {
- poll = draft.data.poll;
+ poll.value = draft.data.poll;
}
}
}
@@ -923,21 +938,21 @@ onMounted(() => {
// 削除して編集
if (props.initialNote) {
const init = props.initialNote;
- text = init.text ? init.text : '';
- files = init.files;
- cw = init.cw;
- useCw = init.cw != null;
+ text.value = init.text ? init.text : '';
+ files.value = init.files;
+ cw.value = init.cw;
+ useCw.value = init.cw != null;
if (init.poll) {
- poll = {
+ poll.value = {
choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple,
expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null,
expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null,
};
}
- visibility = init.visibility;
- localOnly = init.localOnly;
- quoteId = init.renote ? init.renote.id : null;
+ visibility.value = init.visibility;
+ localOnly.value = init.localOnly;
+ quoteId.value = init.renote ? init.renote.id : null;
}
nextTick(() => watchForDraft());
@@ -1031,6 +1046,16 @@ defineExpose({
}
}
+.colorBar {
+ position: absolute;
+ top: 0px;
+ left: 12px;
+ width: 5px;
+ height: 100% ;
+ border-radius: 999px;
+ pointer-events: none;
+}
+
.submitInner {
padding: 0 12px;
line-height: 34px;
@@ -1066,8 +1091,9 @@ defineExpose({
.visibility {
overflow: clip;
- text-overflow: ellipsis;
- white-space: nowrap;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 210px;
&:enabled {
> .headerRightButtonText {
@@ -1288,5 +1314,6 @@ defineExpose({
.headerRight {
gap: 0;
}
+
}
</style>
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index 25a8788a38..cd25077bfb 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue';
@@ -22,6 +22,7 @@ const props = defineProps<{
mention?: Misskey.entities.User;
specified?: Misskey.entities.User;
initialText?: string;
+ initialCw?: string;
initialVisibility?: typeof Misskey.noteVisibilities;
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
@@ -37,11 +38,11 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let modal = $shallowRef<InstanceType<typeof MkModal>>();
-let form = $shallowRef<InstanceType<typeof MkPostForm>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
+const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() {
- modal.close({
+ modal.value.close({
useSendAnimation: true,
});
}
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 9f50f7ad5d..e963697997 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -23,8 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, watch } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
@@ -35,15 +34,15 @@ const RELEASE_TRANSITION_DURATION = 200;
const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 170;
-let isPullStart = $ref(false);
-let isPullEnd = $ref(false);
-let isRefreshing = $ref(false);
-let pullDistance = $ref(0);
+const isPullStart = ref(false);
+const isPullEnd = ref(false);
+const isRefreshing = ref(false);
+const pullDistance = ref(0);
let supportPointerDesktop = false;
let startScreenY: number | null = null;
-const rootEl = $shallowRef<HTMLDivElement>();
+const rootEl = shallowRef<HTMLDivElement>();
let scrollEl: HTMLElement | null = null;
let disabled = false;
@@ -66,17 +65,17 @@ function getScreenY(event) {
}
function moveStart(event) {
- if (!isPullStart && !isRefreshing && !disabled) {
- isPullStart = true;
+ if (!isPullStart.value && !isRefreshing.value && !disabled) {
+ isPullStart.value = true;
startScreenY = getScreenY(event);
- pullDistance = 0;
+ pullDistance.value = 0;
}
}
function moveBySystem(to: number): Promise<void> {
return new Promise(r => {
- const startHeight = pullDistance;
- const overHeight = pullDistance - to;
+ const startHeight = pullDistance.value;
+ const overHeight = pullDistance.value - to;
if (overHeight < 1) {
r();
return;
@@ -85,36 +84,36 @@ function moveBySystem(to: number): Promise<void> {
let intervalId = setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
- pullDistance = to;
+ pullDistance.value = to;
clearInterval(intervalId);
r();
return;
}
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
- if (pullDistance < nextHeight) return;
- pullDistance = nextHeight;
+ if (pullDistance.value < nextHeight) return;
+ pullDistance.value = nextHeight;
}, 1);
});
}
async function fixOverContent() {
- if (pullDistance > FIRE_THRESHOLD) {
+ if (pullDistance.value > FIRE_THRESHOLD) {
await moveBySystem(FIRE_THRESHOLD);
}
}
async function closeContent() {
- if (pullDistance > 0) {
+ if (pullDistance.value > 0) {
await moveBySystem(0);
}
}
function moveEnd() {
- if (isPullStart && !isRefreshing) {
+ if (isPullStart.value && !isRefreshing.value) {
startScreenY = null;
- if (isPullEnd) {
- isPullEnd = false;
- isRefreshing = true;
+ if (isPullEnd.value) {
+ isPullEnd.value = false;
+ isRefreshing.value = true;
fixOverContent().then(() => {
emit('refresh');
props.refresher().then(() => {
@@ -122,17 +121,17 @@ function moveEnd() {
});
});
} else {
- closeContent().then(() => isPullStart = false);
+ closeContent().then(() => isPullStart.value = false);
}
}
}
function moving(event: TouchEvent | PointerEvent) {
- if (!isPullStart || isRefreshing || disabled) return;
+ if (!isPullStart.value || isRefreshing.value || disabled) return;
- if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
- pullDistance = 0;
- isPullEnd = false;
+ if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
+ pullDistance.value = 0;
+ isPullEnd.value = false;
moveEnd();
return;
}
@@ -143,13 +142,13 @@ function moving(event: TouchEvent | PointerEvent) {
const moveScreenY = getScreenY(event);
const moveHeight = moveScreenY - startScreenY!;
- pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
+ pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
- if (pullDistance > 0) {
+ if (pullDistance.value > 0) {
if (event.cancelable) event.preventDefault();
}
- isPullEnd = pullDistance >= FIRE_THRESHOLD;
+ isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
}
/**
@@ -159,8 +158,8 @@ function moving(event: TouchEvent | PointerEvent) {
*/
function refreshFinished() {
closeContent().then(() => {
- isPullStart = false;
- isRefreshing = false;
+ isPullStart.value = false;
+ isRefreshing.value = false;
});
}
@@ -182,26 +181,26 @@ function onScrollContainerScroll() {
}
function registerEventListenersForReadyToPull() {
- if (rootEl == null) return;
- rootEl.addEventListener('touchstart', moveStart, { passive: true });
- rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
+ if (rootEl.value == null) return;
+ rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
+ rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
}
function unregisterEventListenersForReadyToPull() {
- if (rootEl == null) return;
- rootEl.removeEventListener('touchstart', moveStart);
- rootEl.removeEventListener('touchmove', moving);
+ if (rootEl.value == null) return;
+ rootEl.value.removeEventListener('touchstart', moveStart);
+ rootEl.value.removeEventListener('touchmove', moving);
}
onMounted(() => {
- if (rootEl == null) return;
+ if (rootEl.value == null) return;
- scrollEl = getScrollContainer(rootEl);
+ scrollEl = getScrollContainer(rootEl.value);
if (scrollEl == null) return;
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
- rootEl.addEventListener('touchend', moveEnd, { passive: true });
+ rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
registerEventListenersForReadyToPull();
});
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index ba64775298..ebbd5e6cdc 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
+import { ref } from 'vue';
import { $i, getAccounts } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js';
@@ -62,26 +63,26 @@ defineProps<{
}>();
// ServiceWorker registration
-let registration = $ref<ServiceWorkerRegistration | undefined>();
+const registration = ref<ServiceWorkerRegistration | undefined>();
// If this browser supports push notification
-let supported = $ref(false);
+const supported = ref(false);
// If this browser has already subscribed to push notification
-let pushSubscription = $ref<PushSubscription | null>(null);
-let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
+const pushSubscription = ref<PushSubscription | null>(null);
+const pushRegistrationInServer = ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
function subscribe() {
- if (!registration || !supported || !instance.swPublickey) return;
+ if (!registration.value || !supported.value || !instance.swPublickey) return;
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
- return promiseDialog(registration.pushManager.subscribe({
+ return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then(async subscription => {
- pushSubscription = subscription;
+ pushSubscription.value = subscription;
// Register
- pushRegistrationInServer = await api('sw/register', {
+ pushRegistrationInServer.value = await api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh')),
@@ -102,12 +103,12 @@ function subscribe() {
}
async function unsubscribe() {
- if (!pushSubscription) return;
+ if (!pushSubscription.value) return;
- const endpoint = pushSubscription.endpoint;
+ const endpoint = pushSubscription.value.endpoint;
const accounts = await getAccounts();
- pushRegistrationInServer = undefined;
+ pushRegistrationInServer.value = undefined;
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
@@ -115,11 +116,11 @@ async function unsubscribe() {
endpoint,
});
} else {
- pushSubscription.unsubscribe();
+ pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
});
- pushSubscription = null;
+ pushSubscription.value = null;
}
}
@@ -150,20 +151,20 @@ if (navigator.serviceWorker == null) {
// TODO: よしなに?
} else {
navigator.serviceWorker.ready.then(async swr => {
- registration = swr;
+ registration.value = swr;
- pushSubscription = await registration.pushManager.getSubscription();
+ pushSubscription.value = await registration.value.pushManager.getSubscription();
if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
- supported = true;
+ supported.value = true;
- if (pushSubscription) {
+ if (pushSubscription.value) {
const res = await api('sw/show-registration', {
- endpoint: pushSubscription.endpoint,
+ endpoint: pushSubscription.value.endpoint,
});
if (res) {
- pushRegistrationInServer = res;
+ pushRegistrationInServer.value = res;
}
}
}
@@ -171,6 +172,6 @@ if (navigator.serviceWorker == null) {
}
defineExpose({
- pushRegistrationInServer: $$(pushRegistrationInServer),
+ pushRegistrationInServer: pushRegistrationInServer,
});
</script>
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index f22774ef97..edb3abe5f7 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
const props = defineProps<{
modelValue: any;
@@ -36,7 +36,7 @@ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void;
}>();
-let checked = $computed(() => props.modelValue === props.value);
+const checked = computed(() => props.modelValue === props.value);
function toggle(): void {
if (props.disabled) return;
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 88e262d880..75eb91e7ad 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
@@ -27,13 +27,13 @@ const emit = defineEmits<{
(ev: 'end'): void;
}>();
-let up = $ref(false);
+const up = ref(false);
const zIndex = os.claimZIndex('middle');
const angle = (90 - (Math.random() * 180)) + 'deg';
onMounted(() => {
window.setTimeout(() => {
- up = true;
+ up.value = true;
}, 10);
window.setTimeout(() => {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 42b5243e94..e7901316a2 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
@click="toggleReaction()"
>
- <MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/>
+ <MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]" @click="toggleReaction()"/>
<span :class="$style.count">{{ count }}</span>
</button>
</template>
@@ -28,6 +28,7 @@ import MkReactionEffect from '@/components/MkReactionEffect.vue';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import * as sound from '@/scripts/sound.js';
const props = defineProps<{
reaction: string;
@@ -59,6 +60,10 @@ async function toggleReaction() {
});
if (confirm.canceled) return;
+ if (oldReaction !== props.reaction) {
+ sound.play('reaction');
+ }
+
if (mock) {
emit('reactionToggled', props.reaction, (props.count - 1));
return;
@@ -75,6 +80,8 @@ async function toggleReaction() {
}
});
} else {
+ sound.play('reaction');
+
if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1));
return;
@@ -132,12 +139,14 @@ if (!mock) {
<style lang="scss" module>
.root {
- display: inline-block;
+ display: inline-flex;
height: 42px;
margin: 2px;
padding: 0 6px;
font-size: 1.5em;
border-radius: var(--radius-sm);
+ align-items: center;
+ justify-content: center;
&.canToggle {
background: var(--buttonBg);
@@ -176,7 +185,7 @@ if (!mock) {
&.reacted, &.reacted:hover {
background: var(--accentedBg);
color: var(--accent);
- box-shadow: 0 0 0px 1px var(--accent) inset;
+ box-shadow: 0 0 0 1px var(--accent) inset;
> .count {
color: var(--accent);
@@ -188,7 +197,7 @@ if (!mock) {
}
}
-.icon {
+.limitWidth {
max-width: 150px;
}
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 13d022977e..d2a5c431fe 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { inject, watch } from 'vue';
+import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js';
@@ -38,31 +38,31 @@ const emit = defineEmits<{
const initialReactions = new Set(Object.keys(props.note.reactions));
-let reactions = $ref<[string, number][]>([]);
-let hasMoreReactions = $ref(false);
+const reactions = ref<[string, number][]>([]);
+const hasMoreReactions = ref(false);
-if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReaction)) {
- reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction];
+if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
+ reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
}
function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return;
- const i = reactions.findIndex((item) => item[0] === emoji);
+ const i = reactions.value.findIndex((item) => item[0] === emoji);
if (i < 0) return;
- emit('mockUpdateMyReaction', emoji, (count - reactions[i][1]));
+ emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1]));
}
watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
let newReactions: [string, number][] = [];
- hasMoreReactions = Object.keys(newSource).length > maxNumber;
+ hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
- for (let i = 0; i < reactions.length; i++) {
- const reaction = reactions[i][0];
+ for (let i = 0; i < reactions.value.length; i++) {
+ const reaction = reactions.value[i][0];
if (reaction in newSource && newSource[reaction] !== 0) {
- reactions[i][1] = newSource[reaction];
- newReactions.push(reactions[i]);
+ reactions.value[i][1] = newSource[reaction];
+ newReactions.push(reactions.value[i]);
}
}
@@ -80,7 +80,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
}
- reactions = newReactions;
+ reactions.value = newReactions;
}, { immediate: true, deep: true });
</script>
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 3dc9a94ae2..e69aa1be80 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick } from 'vue';
+import { onMounted, nextTick, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -23,11 +23,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const rootEl = $shallowRef<HTMLDivElement>(null);
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
@@ -38,8 +38,8 @@ async function renderChart() {
chartInstance.destroy();
}
- const wide = rootEl.offsetWidth > 600;
- const narrow = rootEl.offsetWidth < 400;
+ const wide = rootEl.value.offsetWidth > 600;
+ const narrow = rootEl.value.offsetWidth < 400;
const maxDays = wide ? 10 : narrow ? 5 : 7;
@@ -66,7 +66,7 @@ async function renderChart() {
}
}
- fetching = false;
+ fetching.value = false;
await nextTick();
@@ -83,7 +83,7 @@ async function renderChart() {
const marginEachCell = 12;
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index a8718b98d6..08830fca7a 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
@@ -62,17 +62,17 @@ import * as os from '@/os.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
-let signing = $ref(false);
-let user = $ref<Misskey.entities.UserDetailed | null>(null);
-let username = $ref('');
-let password = $ref('');
-let token = $ref('');
-let host = $ref(toUnicode(configHost));
-let totpLogin = $ref(false);
-let queryingKey = $ref(false);
-let credentialRequest = $ref<CredentialRequestOptions | null>(null);
-let hCaptchaResponse = $ref(null);
-let reCaptchaResponse = $ref(null);
+const signing = ref(false);
+const user = ref<Misskey.entities.UserDetailed | null>(null);
+const username = ref('');
+const password = ref('');
+const token = ref('');
+const host = ref(toUnicode(configHost));
+const totpLogin = ref(false);
+const queryingKey = ref(false);
+const credentialRequest = ref<CredentialRequestOptions | null>(null);
+const hCaptchaResponse = ref(null);
+const reCaptchaResponse = ref(null);
const emit = defineEmits<{
(ev: 'login', v: any): void;
@@ -98,11 +98,11 @@ const props = defineProps({
function onUsernameChange(): void {
os.api('users/show', {
- username: username,
+ username: username.value,
}).then(userResponse => {
- user = userResponse;
+ user.value = userResponse;
}, () => {
- user = null;
+ user.value = null;
});
}
@@ -113,21 +113,21 @@ function onLogin(res: any): Promise<void> | void {
}
async function queryKey(): Promise<void> {
- queryingKey = true;
- await webAuthnRequest(credentialRequest)
+ queryingKey.value = true;
+ await webAuthnRequest(credentialRequest.value)
.catch(() => {
- queryingKey = false;
+ queryingKey.value = false;
return Promise.reject(null);
}).then(credential => {
- credentialRequest = null;
- queryingKey = false;
- signing = true;
+ credentialRequest.value = null;
+ queryingKey.value = false;
+ signing.value = true;
return os.api('signin', {
- username,
- password,
+ username: username.value,
+ password: password.value,
credential: credential.toJSON(),
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
});
}).then(res => {
emit('login', res);
@@ -138,39 +138,39 @@ async function queryKey(): Promise<void> {
type: 'error',
text: i18n.ts.signinFailed,
});
- signing = false;
+ signing.value = false;
});
}
function onSubmit(): void {
- signing = true;
- if (!totpLogin && user && user.twoFactorEnabled) {
- if (webAuthnSupported() && user.securityKeys) {
+ signing.value = true;
+ if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
+ if (webAuthnSupported() && user.value.securityKeys) {
os.api('signin', {
- username,
- password,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
+ username: username.value,
+ password: password.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
}).then(res => {
- totpLogin = true;
- signing = false;
- credentialRequest = parseRequestOptionsFromJSON({
+ totpLogin.value = true;
+ signing.value = false;
+ credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res,
});
})
.then(() => queryKey())
.catch(loginFailed);
} else {
- totpLogin = true;
- signing = false;
+ totpLogin.value = true;
+ signing.value = false;
}
} else {
os.api('signin', {
- username,
- password,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
- token: user?.twoFactorEnabled ? token : undefined,
+ username: username.value,
+ password: password.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
+ token: user.value?.twoFactorEnabled ? token.value : undefined,
}).then(res => {
emit('login', res);
onLogin(res);
@@ -218,8 +218,8 @@ function loginFailed(err: any): void {
}
}
- totpLogin = false;
- signing = false;
+ totpLogin.value = false;
+ signing.value = false;
}
function resetPassword(): void {
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 05cef6ed3b..6f961cff05 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@@ -39,15 +39,15 @@ const emit = defineEmits<{
(ev: 'cancelled'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
function onClose() {
emit('cancelled');
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
function onLogin(res) {
emit('done', res);
- if (dialog) dialog.close();
+ if (dialog.value) dialog.value.close();
}
</script>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 389acb82bc..b46dc4bd93 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -80,11 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
-import MkSwitch from './MkSwitch.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@/config.js';
import * as os from '@/os.js';
@@ -106,35 +105,35 @@ const emit = defineEmits<{
const host = toUnicode(config.host);
-let hcaptcha = $ref<Captcha | undefined>();
-let recaptcha = $ref<Captcha | undefined>();
-let turnstile = $ref<Captcha | undefined>();
+const hcaptcha = ref<Captcha | undefined>();
+const recaptcha = ref<Captcha | undefined>();
+const turnstile = ref<Captcha | undefined>();
-let username: string = $ref('');
-let password: string = $ref('');
-let retypedPassword: string = $ref('');
-let invitationCode: string = $ref('');
-let reason: string = $ref('');
-let email = $ref('');
-let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
-let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
-let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
-let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
-let submitting: boolean = $ref(false);
-let hCaptchaResponse = $ref(null);
-let reCaptchaResponse = $ref(null);
-let turnstileResponse = $ref(null);
-let usernameAbortController: null | AbortController = $ref(null);
-let emailAbortController: null | AbortController = $ref(null);
+const username = ref<string>('');
+const password = ref<string>('');
+const retypedPassword = ref<string>('');
+const invitationCode = ref<string>('');
+const reason = ref<string>('');
+const email = ref('');
+const usernameState = ref<null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range'>(null);
+const emailState = ref<null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error'>(null);
+const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>('');
+const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
+const submitting = ref<boolean>(false);
+const hCaptchaResponse = ref(null);
+const reCaptchaResponse = ref(null);
+const turnstileResponse = ref(null);
+const usernameAbortController = ref<null | AbortController>(null);
+const emailAbortController = ref<null | AbortController>(null);
-const shouldDisableSubmitting = $computed((): boolean => {
- return submitting ||
- instance.enableHcaptcha && !hCaptchaResponse ||
- instance.enableRecaptcha && !reCaptchaResponse ||
- instance.enableTurnstile && !turnstileResponse ||
- instance.emailRequiredForSignup && emailState !== 'ok' ||
- usernameState !== 'ok' ||
- passwordRetypeState !== 'match';
+const shouldDisableSubmitting = computed((): boolean => {
+ return submitting.value ||
+ instance.enableHcaptcha && !hCaptchaResponse.value ||
+ instance.enableRecaptcha && !reCaptchaResponse.value ||
+ instance.enableTurnstile && !turnstileResponse.value ||
+ instance.emailRequiredForSignup && emailState.value !== 'ok' ||
+ usernameState.value !== 'ok' ||
+ passwordRetypeState.value !== 'match';
});
function getPasswordStrength(source: string): number {
@@ -162,57 +161,57 @@ function getPasswordStrength(source: string): number {
}
function onChangeUsername(): void {
- if (username === '') {
- usernameState = null;
+ if (username.value === '') {
+ usernameState.value = null;
return;
}
{
const err =
- !username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
- username.length < 1 ? 'min-range' :
- username.length > 20 ? 'max-range' :
+ !username.value.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
+ username.value.length < 1 ? 'min-range' :
+ username.value.length > 20 ? 'max-range' :
null;
if (err) {
- usernameState = err;
+ usernameState.value = err;
return;
}
}
- if (usernameAbortController != null) {
- usernameAbortController.abort();
+ if (usernameAbortController.value != null) {
+ usernameAbortController.value.abort();
}
- usernameState = 'wait';
- usernameAbortController = new AbortController();
+ usernameState.value = 'wait';
+ usernameAbortController.value = new AbortController();
os.api('username/available', {
- username,
- }, undefined, usernameAbortController.signal).then(result => {
- usernameState = result.available ? 'ok' : 'unavailable';
+ username: username.value,
+ }, undefined, usernameAbortController.value.signal).then(result => {
+ usernameState.value = result.available ? 'ok' : 'unavailable';
}).catch((err) => {
if (err.name !== 'AbortError') {
- usernameState = 'error';
+ usernameState.value = 'error';
}
});
}
function onChangeEmail(): void {
- if (email === '') {
- emailState = null;
+ if (email.value === '') {
+ emailState.value = null;
return;
}
- if (emailAbortController != null) {
- emailAbortController.abort();
+ if (emailAbortController.value != null) {
+ emailAbortController.value.abort();
}
- emailState = 'wait';
- emailAbortController = new AbortController();
+ emailState.value = 'wait';
+ emailAbortController.value = new AbortController();
os.api('email-address/available', {
- emailAddress: email,
- }, undefined, emailAbortController.signal).then(result => {
- emailState = result.available ? 'ok' :
+ emailAddress: email.value,
+ }, undefined, emailAbortController.value.signal).then(result => {
+ emailState.value = result.available ? 'ok' :
result.reason === 'used' ? 'unavailable:used' :
result.reason === 'format' ? 'unavailable:format' :
result.reason === 'disposable' ? 'unavailable:disposable' :
@@ -221,50 +220,49 @@ function onChangeEmail(): void {
'unavailable';
}).catch((err) => {
if (err.name !== 'AbortError') {
- emailState = 'error';
+ emailState.value = 'error';
}
});
}
function onChangePassword(): void {
- if (password === '') {
- passwordStrength = '';
+ if (password.value === '') {
+ passwordStrength.value = '';
return;
}
- const strength = getPasswordStrength(password);
- passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
+ const strength = getPasswordStrength(password.value);
+ passwordStrength.value = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
}
function onChangePasswordRetype(): void {
- if (retypedPassword === '') {
- passwordRetypeState = null;
+ if (retypedPassword.value === '') {
+ passwordRetypeState.value = null;
return;
}
- passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
+ passwordRetypeState.value = password.value === retypedPassword.value ? 'match' : 'not-match';
}
async function onSubmit(): Promise<void> {
- if (submitting) return;
- submitting = true;
+ if (submitting.value) return;
+ submitting.value = true;
try {
await os.api('signup', {
- username,
- password,
- emailAddress: email,
- invitationCode,
- reason,
- 'hcaptcha-response': hCaptchaResponse,
- 'g-recaptcha-response': reCaptchaResponse,
- 'turnstile-response': turnstileResponse,
+ username: username.value,
+ password: password.value,
+ emailAddress: email.value,
+ invitationCode: invitationCode.value,
+ reason: reason.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
});
if (instance.emailRequiredForSignup) {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
- text: i18n.t('_signup.emailSent', { email }),
+ text: i18n.t('_signup.emailSent', { email: email.value }),
});
emit('signupEmailPending');
} else if (instance.approvalRequiredForSignup) {
@@ -276,8 +274,8 @@ async function onSubmit(): Promise<void> {
emit('approvalPending');
} else {
const res = await os.api('signin', {
- username,
- password,
+ username: username.value,
+ password: password.value,
});
emit('signup', res);
@@ -286,10 +284,10 @@ async function onSubmit(): Promise<void> {
}
}
} catch {
- submitting = false;
- hcaptcha?.reset?.();
- recaptcha?.reset?.();
- turnstile?.reset?.();
+ submitting.value = false;
+ hcaptcha.value?.reset?.();
+ recaptcha.value?.reset?.();
+ turnstile.value?.reset?.();
os.alert({
type: 'error',
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 09eac0732a..bc4fec305b 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => {
} else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy;
} else {
- return "";
+ return '';
}
});
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 73d3b644e9..c8020c6636 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -33,13 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import { $ref } from 'vue/macros';
+import { shallowRef, ref } from 'vue';
+
import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
const props = withDefaults(defineProps<{
autoSet?: boolean;
@@ -52,17 +51,17 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const isAcceptedServerRule = $ref(false);
+const isAcceptedServerRule = ref(false);
function onSignup(res) {
emit('done', res);
- dialog.close();
+ dialog.value.close();
}
function onSignupEmailPending() {
- dialog.close();
+ dialog.value.close();
}
function onApprovalPending() {
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index a91f1f444c..c071fb938a 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="{ [$style.clickToOpen]: defaultStore.state.clickToOpen }" @click="defaultStore.state.clickToOpen ? noteclick(note.id) : undefined">
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
- <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" v-on:click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
+ <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
- <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="note.text && translating || note.text && translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else>
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
</div>
</div>
- <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" v-on:click.stop>RN: ...</MkA>
+ <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA>
</div>
<details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles">
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
@@ -39,14 +39,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import * as mfm from '@sharkey/sfm-js';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
import { defaultStore } from '@/store.js';
import { useRouter } from '@/router.js';
@@ -69,25 +68,25 @@ function noteclick(id: string) {
}
}
-const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-let allowAnim = $ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+const parsed = computed(() => props.note.text ? mfm.parse(props.note.text) : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+let allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
const isLong = defaultStore.state.expandLongNote && !props.hideFiles ? false : shouldCollapsed(props.note, []);
function animatedMFM() {
- if (allowAnim) {
- allowAnim = false;
+ if (allowAnim.value) {
+ allowAnim.value = false;
} else {
os.confirm({
type: 'warning',
text: i18n.ts._animatedMFM._alert.text,
okText: i18n.ts._animatedMFM._alert.confirm,
- }).then((res) => { if (!res.canceled) allowAnim = true; });
+ }).then((res) => { if (!res.canceled) allowAnim.value = true; });
}
}
-const collapsed = $ref(isLong);
+const collapsed = ref(isLong);
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index 7521bd6c76..35e5aebbdd 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
+<div :class="[$style.root, { [$style.disabled]: disabled }]">
<input
ref="input"
type="checkbox"
@@ -64,9 +64,6 @@ const toggle = () => {
opacity: 0.6;
cursor: not-allowed;
}
-
- //&.checked {
- //}
}
.input {
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index a3d82fee5e..083c34906f 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, watch, onBeforeUnmount } from 'vue';
+import { onMounted, watch, onBeforeUnmount, ref, shallowRef } from 'vue';
import tinycolor from 'tinycolor2';
const loaded = !!window.TagCanvas;
@@ -23,13 +23,13 @@ const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const computedStyle = getComputedStyle(document.documentElement);
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
-let available = $ref(false);
-let rootEl = $shallowRef<HTMLElement | null>(null);
-let canvasEl = $shallowRef<HTMLCanvasElement | null>(null);
-let tagsEl = $shallowRef<HTMLElement | null>(null);
-let width = $ref(300);
+const available = ref(false);
+const rootEl = shallowRef<HTMLElement | null>(null);
+const canvasEl = shallowRef<HTMLCanvasElement | null>(null);
+const tagsEl = shallowRef<HTMLElement | null>(null);
+const width = ref(300);
-watch($$(available), () => {
+watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
@@ -52,15 +52,15 @@ watch($$(available), () => {
});
onMounted(() => {
- width = rootEl.offsetWidth;
+ width.value = rootEl.value.offsetWidth;
if (loaded) {
- available = true;
+ available.value = true;
} else {
document.head.appendChild(Object.assign(document.createElement('script'), {
async: true,
src: '/client-assets/tagcanvas.min.js',
- })).addEventListener('load', () => available = true);
+ })).addEventListener('load', () => available.value = true);
}
});
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index c35274959e..5c70adde11 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
- :autocomplete="autocomplete"
+ :autocomplete="props.autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@@ -26,16 +26,21 @@ SPDX-License-Identifier: AGPL-3.0-only
></textarea>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
+ <button v-if="mfmPreview" style="font-size: 0.85em;" class="_textButton" type="button" @click="preview = !preview">{{ i18n.ts.preview }}</button>
+ <div v-if="mfmPreview" v-show="preview" v-panel :class="$style.mfmPreview">
+ <Mfm :text="v"/>
+ </div>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
+import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
+import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{
modelValue: string | null;
@@ -46,6 +51,8 @@ const props = defineProps<{
placeholder?: string;
autofocus?: boolean;
autocomplete?: string;
+ mfmAutocomplete?: boolean | SuggestionType[],
+ mfmPreview?: boolean;
spellcheck?: boolean;
debounce?: boolean;
manualSave?: boolean;
@@ -68,6 +75,8 @@ const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLTextAreaElement>();
+const preview = ref(false);
+let autocomplete: Autocomplete;
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -82,6 +91,16 @@ const onKeydown = (ev: KeyboardEvent) => {
if (ev.code === 'Enter') {
emit('enter');
}
+
+ if (props.code && ev.key === 'Tab') {
+ const pos = inputEl.value?.selectionStart ?? 0;
+ const posEnd = inputEl.value?.selectionEnd ?? v.value.length;
+ v.value = v.value.slice(0, pos) + '\t' + v.value.slice(posEnd);
+ nextTick(() => {
+ inputEl.value?.setSelectionRange(pos + 1, pos + 1);
+ });
+ ev.preventDefault();
+ }
};
const updated = () => {
@@ -113,6 +132,16 @@ onMounted(() => {
focus();
}
});
+
+ if (props.mfmAutocomplete) {
+ autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+ }
+});
+
+onUnmounted(() => {
+ if (autocomplete) {
+ autocomplete.detach();
+ }
});
</script>
@@ -194,4 +223,12 @@ onMounted(() => {
.save {
margin: 8px 0 0 0;
}
+
+.mfmPreview {
+ padding: 12px;
+ border-radius: var(--radius);
+ box-sizing: border-box;
+ min-height: 130px;
+ pointer-events: none;
+}
</style>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 85096dc583..8bd68c0fd2 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, watch, onUnmounted, provide } from 'vue';
+import { computed, watch, onUnmounted, provide, ref } from 'vue';
import { Connection } from 'misskey-js/built/streaming.js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
@@ -65,8 +65,8 @@ type TimelineQueryType = {
roleId?: string
}
-const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
-const tlComponent: InstanceType<typeof MkNotes> = $ref();
+const prComponent = ref<InstanceType<typeof MkPullToRefresh>>();
+const tlComponent = ref<InstanceType<typeof MkNotes>>();
let tlNotesCount = 0;
@@ -77,7 +77,7 @@ const prepend = note => {
note._shouldInsertAd_ = true;
}
- tlComponent.pagingComponent?.prepend(note);
+ tlComponent.value.pagingComponent?.prepend(note);
emit('note');
@@ -271,7 +271,7 @@ function reloadTimeline() {
return new Promise<void>((res) => {
tlNotesCount = 0;
- tlComponent.pagingComponent?.reload().then(() => {
+ tlComponent.value.pagingComponent?.reload().then(() => {
res();
});
});
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index 3b26b50a0b..82cd236193 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -35,11 +35,11 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('high');
-let showing = $ref(true);
+const showing = ref(true);
onMounted(() => {
window.setTimeout(() => {
- showing = false;
+ showing.value = false;
}, 4000);
});
</script>
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 8958accc4a..f5fa86a908 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from './MkInput.vue';
import MkSwitch from './MkSwitch.vue';
@@ -67,37 +67,37 @@ const emit = defineEmits<{
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
}>();
-const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-let name = $ref(props.initialName);
-let permissions = $ref({});
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const name = ref(props.initialName);
+const permissions = ref({});
if (props.initialPermissions) {
for (const kind of props.initialPermissions) {
- permissions[kind] = true;
+ permissions.value[kind] = true;
}
} else {
for (const kind of Misskey.permissions) {
- permissions[kind] = false;
+ permissions.value[kind] = false;
}
}
function ok(): void {
emit('done', {
- name: name,
- permissions: Object.keys(permissions).filter(p => permissions[p]),
+ name: name.value,
+ permissions: Object.keys(permissions.value).filter(p => permissions.value[p]),
});
- dialog.close();
+ dialog.value.close();
}
function disableAll(): void {
- for (const p in permissions) {
- permissions[p] = false;
+ for (const p in permissions.value) {
+ permissions.value[p] = false;
}
}
function enableAll(): void {
- for (const p in permissions) {
- permissions[p] = true;
+ for (const p in permissions.value) {
+ permissions.value[p] = true;
}
}
</script>
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 421c0a8af8..c2384423fd 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.divider"></div>
<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
<template #link>
- <a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+ <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 5db2cc100a..a734f93ec9 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link>
- <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+ <a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 78c62e1250..486aaa0bbd 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<iframe
ref="tweet"
allow="fullscreen;web-share"
- sandbox="allow-popups allow-scripts allow-same-origin"
+ sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
scrolling="no"
:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
- <div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
+ <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
</div>
<article :class="$style.body">
<header :class="$style.header">
@@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onUnmounted } from 'vue';
+import { defineAsyncComponent, onUnmounted, ref } from 'vue';
import type { summaly } from 'summaly';
import { url as local } from '@/config.js';
import { i18n } from '@/i18n.js';
@@ -107,35 +107,36 @@ const props = withDefaults(defineProps<{
});
const MOBILE_THRESHOLD = 500;
-const isMobile = $ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
+const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
const self = props.url.startsWith(local);
const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
-let fetching = $ref(true);
-let title = $ref<string | null>(null);
-let description = $ref<string | null>(null);
-let thumbnail = $ref<string | null>(null);
-let icon = $ref<string | null>(null);
-let sitename = $ref<string | null>(null);
-let player = $ref({
+const fetching = ref(true);
+const title = ref<string | null>(null);
+const description = ref<string | null>(null);
+const thumbnail = ref<string | null>(null);
+const icon = ref<string | null>(null);
+const sitename = ref<string | null>(null);
+const sensitive = ref<boolean>(false);
+const player = ref({
url: null,
width: null,
height: null,
} as SummalyResult['player']);
-let playerEnabled = $ref(false);
-let tweetId = $ref<string | null>(null);
-let tweetExpanded = $ref(props.detail);
+const playerEnabled = ref(false);
+const tweetId = ref<string | null>(null);
+const tweetExpanded = ref(props.detail);
const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
-let tweetHeight = $ref(150);
-let unknownUrl = $ref(false);
+const tweetHeight = ref(150);
+const unknownUrl = ref(false);
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com' || requestUrl.hostname === 'x.com' || requestUrl.hostname === 'mobile.x.com') {
const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
- if (m) tweetId = m[1];
+ if (m) tweetId.value = m[1];
}
if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
@@ -147,8 +148,8 @@ requestUrl.hash = '';
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`)
.then(res => {
if (!res.ok) {
- fetching = false;
- unknownUrl = true;
+ fetching.value = false;
+ unknownUrl.value = true;
return;
}
@@ -156,20 +157,21 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
})
.then((info: SummalyResult) => {
if (info.url == null) {
- fetching = false;
- unknownUrl = true;
+ fetching.value = false;
+ unknownUrl.value = true;
return;
}
- fetching = false;
- unknownUrl = false;
+ fetching.value = false;
+ unknownUrl.value = false;
- title = info.title;
- description = info.description;
- thumbnail = info.thumbnail;
- icon = info.icon;
- sitename = info.sitename;
- player = info.player;
+ title.value = info.title;
+ description.value = info.description;
+ thumbnail.value = info.thumbnail;
+ icon.value = info.icon;
+ sitename.value = info.sitename;
+ player.value = info.player;
+ sensitive.value = info.sensitive ?? false;
});
function adjustTweetHeight(message: any) {
@@ -178,7 +180,7 @@ function adjustTweetHeight(message: any) {
if (embed?.method !== 'twttr.private.resize') return;
if (embed?.id !== embedId) return;
const height = embed?.params[0]?.height;
- if (height) tweetHeight = height;
+ if (height) tweetHeight.value = height;
}
const openPlayer = (): void => {
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index 0ab012dfb7..81c383540c 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -28,16 +28,16 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('middle');
-let top = $ref(0);
-let left = $ref(0);
+const top = ref(0);
+const left = ref(0);
onMounted(() => {
const rect = props.source.getBoundingClientRect();
const x = Math.max((rect.left + (props.source.offsetWidth / 2)) - (300 / 2), 6) + window.pageXOffset;
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
- top = y;
- left = x;
+ top.value = y;
+ left.value = x;
});
</script>
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 42ccb621b6..e1237659c2 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
@@ -66,12 +66,12 @@ const props = defineProps<{
announcement?: any,
}>();
-let dialog = $ref(null);
-let title: string = $ref(props.announcement ? props.announcement.title : '');
-let text: string = $ref(props.announcement ? props.announcement.text : '');
-let icon: string = $ref(props.announcement ? props.announcement.icon : 'info');
-let display: string = $ref(props.announcement ? props.announcement.display : 'dialog');
-let needConfirmationToRead = $ref(props.announcement ? props.announcement.needConfirmationToRead : false);
+const dialog = ref(null);
+const title = ref<string>(props.announcement ? props.announcement.title : '');
+const text = ref<string>(props.announcement ? props.announcement.text : '');
+const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
+const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
+const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
@@ -80,12 +80,12 @@ const emit = defineEmits<{
async function done() {
const params = {
- title: title,
- text: text,
- icon: icon,
+ title: title.value,
+ text: text.value,
+ icon: icon.value,
imageUrl: null,
- display: display,
- needConfirmationToRead: needConfirmationToRead,
+ display: display.value,
+ needConfirmationToRead: needConfirmationToRead.value,
userId: props.user.id,
};
@@ -102,7 +102,7 @@ async function done() {
},
});
- dialog.close();
+ dialog.value.close();
} else {
const created = await os.apiWithDialog('admin/announcements/create', params);
@@ -110,14 +110,14 @@ async function done() {
created: created,
});
- dialog.close();
+ dialog.value.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: title }),
+ text: i18n.t('removeAreYouSure', { x: title.value }),
});
if (canceled) return;
@@ -127,7 +127,7 @@ async function del() {
emit('done', {
deleted: true,
});
- dialog.close();
+ dialog.value.close();
});
}
</script>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 978c5005c8..b9c7377972 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os.js';
import { acct } from '@/filters/user.js';
@@ -28,14 +28,14 @@ const props = withDefaults(defineProps<{
withChart: true,
});
-let chartValues = $ref<number[] | null>(null);
+const chartValues = ref<number[] | null>(null);
onMounted(() => {
if (props.withChart) {
os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res.inc.splice(0, 1);
- chartValues = res.inc;
+ chartValues.value = res.inc;
});
}
});
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 322ffee38e..4e326911d8 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -22,10 +22,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.notes }}</p><span :class="$style.statusItemValue">{{ number(user.notesCount) }}</span>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.following }}</p><span :class="$style.statusItemValue">{{ number(user.followingCount) }}</span>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
</div>
</div>
@@ -40,7 +40,7 @@ import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
defineProps<{
user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index c6e1218c0f..76470cba88 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
@@ -24,7 +24,7 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-const text = $computed(() => {
+const text = computed(() => {
switch (props.user.onlineStatus) {
case 'online': return i18n.ts.online;
case 'active': return i18n.ts.active;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index d958b325e5..ec2c48b1cf 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -47,11 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statusItemLabel">{{ i18n.ts.notes }}</div>
<div>{{ number(user.notesCount) }}</div>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowingVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.following }}</div>
<div>{{ number(user.followingCount) }}</div>
</div>
- <div v-if="isFfVisibleForMe(user)" :class="$style.statusItem">
+ <div v-if="isFollowersVisibleForMe(user)" :class="$style.statusItem">
<div :class="$style.statusItemLabel">{{ i18n.ts.followers }}</div>
<div>{{ number(user.followersCount) }}</div>
</div>
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { userPage } from '@/filters/user.js';
@@ -77,7 +77,7 @@ import number from '@/filters/number.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
-import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
const props = defineProps<{
showing: boolean;
@@ -92,18 +92,18 @@ const emit = defineEmits<{
}>();
const zIndex = os.claimZIndex('middle');
-let user = $ref<Misskey.entities.UserDetailed | null>(null);
-let top = $ref(0);
-let left = $ref(0);
+const user = ref<Misskey.entities.UserDetailed | null>(null);
+const top = ref(0);
+const left = ref(0);
function showMenu(ev: MouseEvent) {
- const { menu, cleanup } = getUserMenu(user);
+ const { menu, cleanup } = getUserMenu(user.value);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
onMounted(() => {
if (typeof props.q === 'object') {
- user = props.q;
+ user.value = props.q;
} else {
const query = props.q.startsWith('@') ?
Misskey.acct.parse(props.q.substring(1)) :
@@ -111,7 +111,7 @@ onMounted(() => {
os.api('users/show', query).then(res => {
if (!props.showing) return;
- user = res;
+ user.value = res;
});
}
@@ -119,8 +119,8 @@ onMounted(() => {
const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
const y = rect.top + props.source.offsetHeight + window.pageYOffset;
- top = y;
- left = x;
+ top.value = y;
+ left.value = x;
});
</script>
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index ac38c4b62f..9d41147bd2 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSplit from '@/components/form/split.vue';
@@ -78,43 +78,43 @@ const props = defineProps<{
includeSelf?: boolean;
}>();
-let username = $ref('');
-let host = $ref('');
-let users: Misskey.entities.UserDetailed[] = $ref([]);
-let recentUsers: Misskey.entities.UserDetailed[] = $ref([]);
-let selected: Misskey.entities.UserDetailed | null = $ref(null);
-let dialogEl = $ref();
+const username = ref('');
+const host = ref('');
+const users = ref<Misskey.entities.UserDetailed[]>([]);
+const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
+const selected = ref<Misskey.entities.UserDetailed | null>(null);
+const dialogEl = ref();
const search = () => {
- if (username === '' && host === '') {
- users = [];
+ if (username.value === '' && host.value === '') {
+ users.value = [];
return;
}
os.api('users/search-by-username-and-host', {
- username: username,
- host: host,
+ username: username.value,
+ host: host.value,
limit: 10,
detail: false,
}).then(_users => {
- users = _users;
+ users.value = _users;
});
};
const ok = () => {
- if (selected == null) return;
- emit('ok', selected);
- dialogEl.close();
+ if (selected.value == null) return;
+ emit('ok', selected.value);
+ dialogEl.value.close();
// 最近使ったユーザー更新
let recents = defaultStore.state.recentlyUsedUsers;
- recents = recents.filter(x => x !== selected.id);
- recents.unshift(selected.id);
+ recents = recents.filter(x => x !== selected.value.id);
+ recents.unshift(selected.value.id);
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
};
const cancel = () => {
emit('cancel');
- dialogEl.close();
+ dialogEl.value.close();
};
onMounted(() => {
@@ -122,9 +122,9 @@ onMounted(() => {
userIds: defaultStore.state.recentlyUsedUsers,
}).then(users => {
if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
- recentUsers = [$i, ...users];
+ recentUsers.value = [$i, ...users];
} else {
- recentUsers = users;
+ recentUsers.value = users;
}
});
});
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 4ecca7334c..5f3f5b81dd 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -34,15 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
-import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
-import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue';
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index 7401dbddb1..664c4da203 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -36,18 +36,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
+import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
-let isLocked = ref(false);
-let hideOnlineStatus = ref(false);
-let noCrawle = ref(false);
+const isLocked = ref(false);
+const hideOnlineStatus = ref(false);
+const noCrawle = ref(false);
watch([isLocked, hideOnlineStatus, noCrawle], () => {
os.api('i/update', {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 8de9bbdbb1..37aa677b44 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -30,8 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import { instance } from '@/instance.js';
+import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index 01a943b7a0..621995cc5b 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -29,7 +29,6 @@ import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
import * as os from '@/os.js';
const props = defineProps<{
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 325829a8a8..61edc345a9 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -42,12 +42,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { nextTick } from 'vue';
+import { nextTick, shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
-const modal = $shallowRef<InstanceType<typeof MkModal>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
@@ -62,13 +62,13 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-let v = $ref(props.currentVisibility);
+const v = ref(props.currentVisibility);
function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
- v = visibility;
+ v.value = visibility;
emit('changeVisibility', visibility);
nextTick(() => {
- if (modal) modal.close();
+ if (modal.value) modal.value.close();
});
}
</script>
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 26de7dee52..746ed3e0de 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted } from 'vue';
+import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2';
@@ -25,11 +25,11 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const chartEl = $shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement>(null);
const now = new Date();
let chartInstance: Chart = null;
const chartLimit = 30;
-let fetching = $ref(true);
+const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
@@ -65,7 +65,7 @@ async function renderChart() {
const max = Math.max(...raw.read);
- chartInstance = new Chart(chartEl, {
+ chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
datasets: [{
@@ -147,7 +147,7 @@ async function renderChart() {
plugins: [chartVLine(vLineColor)],
});
- fetching = false;
+ fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index fe76ded7b4..862a38bd54 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -54,9 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
-import XTimeline from './welcome.timeline.vue';
import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue';
import MkButton from '@/components/MkButton.vue';
@@ -66,20 +65,18 @@ import { instanceName } from '@/config.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import number from '@/filters/number.js';
import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
-let meta = $ref<Misskey.entities.Instance>();
-let stats = $ref(null);
+const meta = ref<Misskey.entities.MetaResponse | null>(null);
+const stats = ref<Misskey.entities.StatsResponse | null>(null);
os.api('meta', { detail: true }).then(_meta => {
- meta = _meta;
+ meta.value = _meta;
});
-os.api('stats', {
-}).then((res) => {
- stats = res;
+os.api('stats', {}).then((res) => {
+ stats.value = res;
});
function signin() {
@@ -107,35 +104,35 @@ function showMenu(ev) {
action: () => {
os.pageWindow('/about-sharkey');
},
- }, null, (instance.impressumUrl) ? {
+ }, { type: 'divider' }, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ph-newspaper-clipping ph-bold ph-lg',
action: () => {
- window.open(instance.impressumUrl, '_blank');
+ window.open(instance.impressumUrl, '_blank', 'noopener');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ph-notebook ph-bold ph-lg',
action: () => {
- window.open(instance.tosUrl, '_blank');
+ window.open(instance.tosUrl, '_blank', 'noopener');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ph-shield ph-bold ph-lg',
action: () => {
- window.open(instance.privacyPolicyUrl, '_blank');
+ window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
},
- } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
+ } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
text: i18n.ts.help,
icon: 'ph-question ph-bold ph-lg',
action: () => {
- window.open('https://misskey-hub.net/help.md', '_blank');
+ window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
},
}], ev.currentTarget ?? ev.target);
}
function exploreOtherServers() {
- window.open('https://joinsharkey.org/#findaninstance', '_blank');
+ window.open('https://joinsharkey.org/#findaninstance', '_blank', 'noopener');
}
</script>
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index d6c3e3f81d..e5b8bd9b15 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -53,10 +53,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onBeforeUnmount, onMounted, provide } from 'vue';
+import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
import contains from '@/scripts/contains.js';
import * as os from '@/os.js';
-import { MenuItem } from '@/types/menu';
+import { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -107,18 +107,18 @@ const emit = defineEmits<{
provide('inWindow', true);
-let rootEl = $shallowRef<HTMLElement | null>();
-let showing = $ref(true);
+const rootEl = shallowRef<HTMLElement | null>();
+const showing = ref(true);
let beforeClickedAt = 0;
-let maximized = $ref(false);
-let minimized = $ref(false);
+const maximized = ref(false);
+const minimized = ref(false);
let unResizedTop = '';
let unResizedLeft = '';
let unResizedWidth = '';
let unResizedHeight = '';
function close() {
- showing = false;
+ showing.value = false;
}
function onKeydown(evt) {
@@ -137,46 +137,46 @@ function onContextmenu(ev: MouseEvent) {
// 最前面へ移動
function top() {
- if (rootEl) {
- rootEl.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+ if (rootEl.value) {
+ rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
}
}
function maximize() {
- maximized = true;
- unResizedTop = rootEl.style.top;
- unResizedLeft = rootEl.style.left;
- unResizedWidth = rootEl.style.width;
- unResizedHeight = rootEl.style.height;
- rootEl.style.top = '0';
- rootEl.style.left = '0';
- rootEl.style.width = '100%';
- rootEl.style.height = '100%';
+ maximized.value = true;
+ unResizedTop = rootEl.value.style.top;
+ unResizedLeft = rootEl.value.style.left;
+ unResizedWidth = rootEl.value.style.width;
+ unResizedHeight = rootEl.value.style.height;
+ rootEl.value.style.top = '0';
+ rootEl.value.style.left = '0';
+ rootEl.value.style.width = '100%';
+ rootEl.value.style.height = '100%';
}
function unMaximize() {
- maximized = false;
- rootEl.style.top = unResizedTop;
- rootEl.style.left = unResizedLeft;
- rootEl.style.width = unResizedWidth;
- rootEl.style.height = unResizedHeight;
+ maximized.value = false;
+ rootEl.value.style.top = unResizedTop;
+ rootEl.value.style.left = unResizedLeft;
+ rootEl.value.style.width = unResizedWidth;
+ rootEl.value.style.height = unResizedHeight;
}
function minimize() {
- minimized = true;
- unResizedWidth = rootEl.style.width;
- unResizedHeight = rootEl.style.height;
- rootEl.style.width = minWidth + 'px';
- rootEl.style.height = props.mini ? '32px' : '39px';
+ minimized.value = true;
+ unResizedWidth = rootEl.value.style.width;
+ unResizedHeight = rootEl.value.style.height;
+ rootEl.value.style.width = minWidth + 'px';
+ rootEl.value.style.height = props.mini ? '32px' : '39px';
}
function unMinimize() {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
- minimized = false;
- rootEl.style.width = unResizedWidth;
- rootEl.style.height = unResizedHeight;
+ minimized.value = false;
+ rootEl.value.style.width = unResizedWidth;
+ rootEl.value.style.height = unResizedHeight;
const browserWidth = window.innerWidth;
const browserHeight = window.innerHeight;
const windowWidth = main.offsetWidth;
@@ -192,7 +192,7 @@ function onBodyMousedown() {
}
function onDblClick() {
- if (minimized) {
+ if (minimized.value) {
unMinimize();
} else {
maximize();
@@ -205,7 +205,7 @@ function onHeaderMousedown(evt: MouseEvent) {
let beforeMaximized = false;
- if (maximized) {
+ if (maximized.value) {
beforeMaximized = true;
unMaximize();
}
@@ -219,7 +219,7 @@ function onHeaderMousedown(evt: MouseEvent) {
beforeClickedAt = Date.now();
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
if (!contains(main, document.activeElement)) main.focus();
@@ -251,8 +251,8 @@ function onHeaderMousedown(evt: MouseEvent) {
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
- rootEl.style.left = moveLeft + 'px';
- rootEl.style.top = moveTop + 'px';
+ rootEl.value.style.left = moveLeft + 'px';
+ rootEl.value.style.top = moveTop + 'px';
}
if (beforeMaximized) {
@@ -270,7 +270,7 @@ function onHeaderMousedown(evt: MouseEvent) {
// 上ハンドル掴み時
function onTopHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
// どういうわけかnullになることがある
if (main == null) return;
@@ -298,7 +298,7 @@ function onTopHandleMousedown(evt) {
// 右ハンドル掴み時
function onRightHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientX;
@@ -323,7 +323,7 @@ function onRightHandleMousedown(evt) {
// 下ハンドル掴み時
function onBottomHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientY;
@@ -348,7 +348,7 @@ function onBottomHandleMousedown(evt) {
// 左ハンドル掴み時
function onLeftHandleMousedown(evt) {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const base = evt.clientX;
@@ -400,27 +400,27 @@ function onBottomLeftHandleMousedown(evt) {
// 高さを適用
function applyTransformHeight(height) {
if (height > window.innerHeight) height = window.innerHeight;
- rootEl.style.height = height + 'px';
+ rootEl.value.style.height = height + 'px';
}
// 幅を適用
function applyTransformWidth(width) {
if (width > window.innerWidth) width = window.innerWidth;
- rootEl.style.width = width + 'px';
+ rootEl.value.style.width = width + 'px';
}
// Y座標を適用
function applyTransformTop(top) {
- rootEl.style.top = top + 'px';
+ rootEl.value.style.top = top + 'px';
}
// X座標を適用
function applyTransformLeft(left) {
- rootEl.style.left = left + 'px';
+ rootEl.value.style.left = left + 'px';
}
function onBrowserResize() {
- const main = rootEl;
+ const main = rootEl.value;
if (main == null) return;
const position = main.getBoundingClientRect();
@@ -438,8 +438,8 @@ onMounted(() => {
applyTransformWidth(props.initialWidth);
if (props.initialHeight) applyTransformHeight(props.initialHeight);
- applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
- applyTransformLeft((window.innerWidth / 2) - (rootEl.offsetWidth / 2));
+ applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
+ applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
top();
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 7460515c33..a9b2e8a00d 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import MkWindow from '@/components/MkWindow.vue';
import { versatileLang } from '@/scripts/intl-const.js';
import { defaultStore } from '@/store.js';
@@ -35,22 +36,22 @@ const props = defineProps<{
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
-let fetching = $ref(true);
-let title = $ref<string | null>(null);
-let player = $ref({
+const fetching = ref(true);
+const title = ref<string | null>(null);
+const player = ref({
url: null,
width: null,
height: null,
});
const ytFetch = (): void => {
- fetching = true;
+ fetching.value = true;
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => {
res.json().then(info => {
if (info.url == null) return;
- title = info.title;
- fetching = false;
- player = info.player;
+ title.value = info.title;
+ fetching.value = false;
+ player.value = info.player;
});
});
};
diff --git a/packages/frontend/src/components/SkApprovalUser.vue b/packages/frontend/src/components/SkApprovalUser.vue
index 99dcb717b1..2bf6361ac8 100644
--- a/packages/frontend/src/components/SkApprovalUser.vue
+++ b/packages/frontend/src/components/SkApprovalUser.vue
@@ -27,6 +27,7 @@
</template>
<script lang="ts" setup>
+import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
@@ -37,15 +38,15 @@ const props = defineProps<{
user: Misskey.entities.User;
}>();
-let reason = $ref('');
-let email = $ref('');
+let reason = ref('');
+let email = ref('');
function getReason() {
return os.api('admin/show-user', {
userId: props.user.id,
}).then(info => {
- reason = info?.signupReason;
- email = info?.email;
+ reason.value = info?.signupReason;
+ email.value = info?.email;
});
}
diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue
index 4e2856388e..fa7b2a444d 100644
--- a/packages/frontend/src/components/SkInstanceTicker.vue
+++ b/packages/frontend/src/components/SkInstanceTicker.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
+import { computed } from 'vue';
import { instanceName } from '@/config.js';
import { instance as Instance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
-const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index b308f4a07a..cb37861330 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted"
+ v-if="!hardMuted && !muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
@@ -58,9 +58,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/>
</p>
- <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
+ <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<Mfm
@@ -81,31 +81,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
</div>
<div v-if="appearNote.files.length > 0">
- <MkMediaList :mediaList="appearNote.files" v-on:click.stop/>
+ <MkMediaList :mediaList="appearNote.files" @click.stop/>
</div>
- <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" v-on:click.stop />
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" v-on:click.stop/>
+ <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
- <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" v-on:click.stop @click="collapsed = false">
+ <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
- <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" v-on:click.stop @click="collapsed = true">
+ <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click.stop @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div>
- <MkReactionsViewer :note="appearNote" :maxNumber="16" v-on:click.stop @mockUpdateMyReaction="emitUpdReaction">
+ <MkReactionsViewer :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction">
<template #more>
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
</template>
</MkReactionsViewer>
<footer :class="$style.footer">
- <button :class="$style.footerButton" class="_button" v-on:click.stop @click="reply()">
+ <button :class="$style.footerButton" class="_button" @click.stop @click="reply()">
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
</button>
@@ -115,7 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.footerButton"
class="_button"
:style="renoted ? 'color: var(--accent) !important;' : ''"
- v-on:click.stop
+ @click.stop
@mousedown="renoted ? undoRenote(appearNote) : boostVisibility()"
>
<i class="ph-rocket-launch ph-bold ph-lg"></i>
@@ -129,19 +129,19 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="quoteButton"
:class="$style.footerButton"
class="_button"
- v-on:click.stop
+ @click.stop
@mousedown="quote()"
>
<i class="ph-quotes ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="like()">
+ <button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop @click="like()">
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
</button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" v-on:click.stop @click="undoReact(appearNote)">
+ <button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click.stop @click="undoReact(appearNote)">
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
@@ -154,7 +154,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -163,10 +163,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</I18n>
</div>
+<div v-else>
+ <!--
+ MkDateSeparatedList uses TransitionGroup which requires single element in the child elements
+ so MkNote create empty div instead of no elements
+ -->
+</div>
</template>
<script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue';
+import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import SkNoteSub from '@/components/SkNoteSub.vue';
@@ -184,6 +190,7 @@ import { focusPrev, focusNext } from '@/scripts/focus.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -207,6 +214,7 @@ const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
pinned?: boolean;
mock?: boolean;
+ withHardMute?: boolean;
}>(), {
mock: false,
});
@@ -223,7 +231,7 @@ const router = useRouter();
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
function noteclick(id: string) {
const selection = document.getSelection();
@@ -235,7 +243,7 @@ function noteclick(id: string) {
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -247,15 +255,16 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.cw == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -267,27 +276,37 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
-const isMyRenote = $i && ($i.id === note.userId);
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
-const urls = $computed(() => parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null);
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
-const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const isLong = shouldCollapsed(appearNote, urls ?? []);
-const collapsed = defaultStore.state.expandLongNote && appearNote.cw == null ? false : ref(appearNote.cw == null && isLong);
+const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text).filter(u => u !== renoteUrl && u !== renoteUri) : null);
+const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
+const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
+const collapsed = defaultStore.state.expandLongNote && appearNote.value.cw == null ? false : ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
const translation = ref<any>(null);
const translating = ref(false);
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
-let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
+const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
+
+function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+ if (mutedWords == null) return false;
+
+ if (checkWordMute(note, $i, mutedWords)) return true;
+ if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
+ if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ return false;
+}
const keymap = {
'r': () => reply(true),
@@ -302,20 +321,20 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
if (props.mock) {
watch(() => props.note, (to) => {
- note = deepClone(to);
+ note.value = deepClone(to);
}, { deep: true });
} else {
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
}
@@ -323,7 +342,7 @@ if (props.mock) {
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -334,14 +353,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -353,14 +372,14 @@ if (!props.mock) {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -420,7 +439,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -431,14 +450,14 @@ function renote(visibility: Visibility | 'local') {
if (!props.mock) {
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
}
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -450,16 +469,16 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
if (!props.mock) {
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -475,13 +494,13 @@ function quote() {
return;
}
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -500,10 +519,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -529,8 +548,8 @@ function reply(viaKeyboard = false): void {
return;
}
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -544,7 +563,7 @@ function like(): void {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -559,13 +578,15 @@ function like(): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
+ sound.play('reaction');
+
if (props.mock) {
return;
}
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -578,16 +599,18 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
if (props.mock) {
emit('reaction', reaction);
return;
}
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -614,8 +637,8 @@ function undoRenote(note) : void {
if (props.mock) {
return;
}
- os.api("notes/unrenote", {
- noteId: note.id
+ os.api('notes/unrenote', {
+ noteId: note.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -649,7 +672,7 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
@@ -659,14 +682,14 @@ function menu(viaKeyboard = false): void {
return;
}
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -677,7 +700,7 @@ async function clip() {
return;
}
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -692,7 +715,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -702,17 +725,17 @@ function showRenoteMenu(viaKeyboard = false): void {
if (isMyRenote) {
pleaseLogin();
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
getUnrenote(),
], renoteTime.value, {
viaKeyboard: viaKeyboard,
});
} else {
os.popupMenu([
- getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
- null,
- getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
+ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
+ { type: 'divider' },
+ getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
], renoteTime.value, {
viaKeyboard: viaKeyboard,
@@ -750,7 +773,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
});
isDeleted.value = true;
}
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 4699eba8f6..8bf9e244e0 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -101,8 +101,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
- <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
+ <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
+ <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
<div v-if="appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
@@ -245,6 +245,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
+import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -256,12 +257,11 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import { MenuItem } from '@/types/menu.js';
import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -272,12 +272,12 @@ const props = defineProps<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+const note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result: Misskey.entities.Note | null = deepClone(note);
+ let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result);
@@ -289,15 +289,15 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result;
+ note.value = result;
});
}
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
@@ -309,26 +309,25 @@ const reactButton = shallowRef<HTMLElement>();
const quoteButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
-
-const isMyRenote = $i && ($i.id === note.userId);
+const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
+const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(defaultStore.state.uncollapseCW);
const isDeleted = ref(false);
const renoted = ref(false);
-const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
+const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref(null);
const translating = ref(false);
-const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
+const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
const urls = parsed ? extractUrlFromMfm(parsed).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
+const animated = computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
const quotes = ref<Misskey.entities.Note[]>([]);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
watch(() => props.expandAllCws, (expandAllCws) => {
@@ -336,8 +335,8 @@ watch(() => props.expandAllCws, (expandAllCws) => {
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -356,41 +355,41 @@ const keymap = {
provide('react', (reaction: string) => {
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
});
-let tab = $ref('replies');
-let reactionTabType = $ref(null);
+const tab = ref('replies');
+const reactionTabType = ref(null);
-const renotesPagination = $computed(() => ({
+const renotesPagination = computed(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
},
}));
-const reactionsPagination = $computed(() => ({
+const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
- noteId: appearNote.id,
- type: reactionTabType,
+ noteId: appearNote.value.id,
+ type: reactionTabType.value,
},
}));
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
- pureNote: $$(note),
+ note: appearNote,
+ pureNote: note,
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
});
@@ -401,14 +400,14 @@ useTooltip(renoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: renoteButton.value,
}, {}, 'closed');
});
useTooltip(quoteButton, async (showing) => {
const renotes = await os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 11,
quote: true,
});
@@ -420,7 +419,7 @@ useTooltip(quoteButton, async (showing) => {
os.popup(MkUsersTooltip, {
showing,
users,
- count: appearNote.renoteCount,
+ count: appearNote.value.renoteCount,
targetElement: quoteButton.value,
}, {}, 'closed');
});
@@ -475,7 +474,7 @@ function renote(visibility: Visibility | 'local') {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -485,13 +484,13 @@ function renote(visibility: Visibility | 'local') {
}
os.api('notes/create', {
- renoteId: appearNote.id,
- channelId: appearNote.channelId,
+ renoteId: appearNote.value.id,
+ channelId: appearNote.value.channelId,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
});
- } else if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) {
+ } else if (!appearNote.value.channel || appearNote.value.channel?.allowRenoteToExternal) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -503,15 +502,15 @@ function renote(visibility: Visibility | 'local') {
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
const localOnlySetting = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
- let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
- if (appearNote.channel?.isSensitive) {
- noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.visibility : visibility, 'home');
+ let noteVisibility = visibility === 'local' || visibility === 'specified' ? smallerVisibility(appearNote.value.visibility, configuredVisibility) : smallerVisibility(visibility, configuredVisibility);
+ if (appearNote.value.channel?.isSensitive) {
+ noteVisibility = smallerVisibility(visibility === 'local' || visibility === 'specified' ? appearNote.value.visibility : visibility, 'home');
}
os.api('notes/create', {
localOnly: visibility === 'local' ? true : localOnlySetting,
visibility: noteVisibility,
- renoteId: appearNote.id,
+ renoteId: appearNote.value.id,
}).then(() => {
os.toast(i18n.ts.renoted);
renoted.value = true;
@@ -523,13 +522,13 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -548,10 +547,10 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
quote: true,
@@ -575,8 +574,8 @@ function reply(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
os.post({
- reply: appearNote,
- channel: appearNote.channel,
+ reply: appearNote.value,
+ channel: appearNote.value.channel,
animation: !viaKeyboard,
}, () => {
focus();
@@ -586,9 +585,9 @@ function reply(viaKeyboard = false): void {
function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
- if (appearNote.reactionAcceptance === 'likeOnly') {
+ if (appearNote.value.reactionAcceptance === 'likeOnly') {
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = reactButton.value as HTMLElement | null | undefined;
@@ -601,11 +600,13 @@ function react(viaKeyboard = false): void {
} else {
blur();
reactionPicker.show(reactButton.value, reaction => {
+ sound.play('reaction');
+
os.api('notes/reactions/create', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
reaction: reaction,
});
- if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) {
+ if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) {
claimAchievement('reactWithoutRead');
}
}, () => {
@@ -618,7 +619,7 @@ function like(): void {
pleaseLogin();
showMovedDialog();
os.api('notes/like', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
override: defaultLike.value,
});
const el = likeButton.value as HTMLElement | null | undefined;
@@ -640,8 +641,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -669,27 +670,27 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
function menu(viaKeyboard = false): void {
- const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function menuVersions(viaKeyboard = false): Promise<void> {
- const { menu, cleanup } = await getNoteVersionsMenu({ note: note, menuVersionsButton });
+ const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton });
os.popupMenu(menu, menuVersionsButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
}
async function clip() {
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
@@ -701,7 +702,7 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
- noteId: note.id,
+ noteId: note.value.id,
});
isDeleted.value = true;
},
@@ -723,7 +724,7 @@ const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
os.api('notes/children', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
showQuotes: false,
}).then(res => {
@@ -738,7 +739,7 @@ const quotesLoaded = ref(false);
function loadQuotes() {
quotesLoaded.value = true;
os.api('notes/renotes', {
- noteId: appearNote.id,
+ noteId: appearNote.value.id,
limit: 30,
quote: true,
}).then(res => {
@@ -753,13 +754,13 @@ const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
os.api('notes/conversation', {
- noteId: appearNote.replyId,
+ noteId: appearNote.value.replyId,
}).then(res => {
conversation.value = res.reverse();
});
}
-if (appearNote.reply && appearNote.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
+if (appearNote.value.reply && appearNote.value.reply.replyId && defaultStore.state.autoloadConversation) loadConversation();
function animatedMFM() {
if (allowAnim.value) {
diff --git a/packages/frontend/src/components/SkNoteSimple.vue b/packages/frontend/src/components/SkNoteSimple.vue
index 05a19e291d..fe12baedeb 100644
--- a/packages/frontend/src/components/SkNoteSimple.vue
+++ b/packages/frontend/src/components/SkNoteSimple.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
- <MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll" @click.stop/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
@@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
-import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
const props = defineProps<{
@@ -36,10 +35,10 @@ const props = defineProps<{
hideFiles?: boolean;
}>();
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
</script>
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index dd4abe8f58..fc30dc87aa 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.content">
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
- <MkCwButton v-model="showContent" :note="note"/>
+ <MkCwButton v-model="showContent" :text="note.text" :files="note.files" :poll="note.poll"/>
</p>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/>
@@ -101,15 +101,14 @@ import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { userPage } from "@/filters/user.js";
-import { checkWordMute } from "@/scripts/check-word-mute.js";
-import { defaultStore } from "@/store.js";
+import { userPage } from '@/filters/user.js';
+import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { defaultStore } from '@/store.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { claimAchievement } from '@/scripts/achievements.js';
-import type { MenuItem } from '@/types/menu.js';
import { getNoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
@@ -140,7 +139,7 @@ const quoteButton = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const likeButton = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
+let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
const isRenote = (
@@ -152,13 +151,13 @@ const isRenote = (
useNoteCapture({
rootEl: el,
- note: $$(appearNote),
+ note: appearNote,
isDeletedRef: isDeleted,
});
if ($i) {
- os.api("notes/renotes", {
- noteId: appearNote.id,
+ os.api('notes/renotes', {
+ noteId: appearNote.value.id,
userId: $i.id,
limit: 1,
}).then((res) => {
@@ -239,8 +238,8 @@ function undoReact(note): void {
function undoRenote() : void {
if (!renoted.value) return;
- os.api("notes/unrenote", {
- noteId: appearNote.id,
+ os.api('notes/unrenote', {
+ noteId: appearNote.value.id,
});
os.toast(i18n.ts.rmboost);
renoted.value = false;
@@ -254,13 +253,13 @@ function undoRenote() : void {
}
}
-let showContent = $ref(defaultStore.state.uncollapseCW);
+let showContent = ref(defaultStore.state.uncollapseCW);
watch(() => props.expandAllCws, (expandAllCws) => {
- if (expandAllCws !== showContent) showContent = expandAllCws;
+ if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
});
-let replies: Misskey.entities.Note[] = $ref([]);
+let replies = ref<Misskey.entities.Note[]>([]);
function boostVisibility() {
os.popupMenu([
@@ -302,7 +301,7 @@ function renote(visibility: 'public' | 'home' | 'followers' | 'specified' | 'loc
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
const el = renoteButton.value as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
@@ -342,12 +341,12 @@ function quote() {
pleaseLogin();
showMovedDialog();
- if (appearNote.channel) {
+ if (appearNote.value.channel) {
os.post({
- renote: appearNote,
- channel: appearNote.channel,
+ renote: appearNote.value,
+ channel: appearNote.value.channel,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -367,9 +366,9 @@ function quote() {
});
} else {
os.post({
- renote: appearNote,
+ renote: appearNote.value,
}).then(() => {
- os.api("notes/renotes", {
+ os.api('notes/renotes', {
noteId: props.note.id,
userId: $i.id,
limit: 1,
@@ -403,7 +402,7 @@ if (props.detail) {
limit: numberOfReplies.value,
showQuotes: false,
}).then(res => {
- replies = res;
+ replies.value = res;
});
}
</script>
diff --git a/packages/frontend/src/components/SkOldNoteWindow.vue b/packages/frontend/src/components/SkOldNoteWindow.vue
index 49fbd39812..237032c9d5 100644
--- a/packages/frontend/src/components/SkOldNoteWindow.vue
+++ b/packages/frontend/src/components/SkOldNoteWindow.vue
@@ -30,7 +30,7 @@
<div :class="$style.noteContent">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
- <MkCwButton v-model="showContent" :note="appearNote"/>
+ <MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
</p>
<div v-show="appearNote.cw == null || showContent">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -76,7 +76,7 @@
</template>
<script lang="ts" setup>
-import { inject, onMounted, ref, shallowRef } from 'vue';
+import { inject, onMounted, ref, shallowRef, computed } from 'vue';
import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -89,7 +89,6 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import { userPage } from '@/filters/user.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/scripts/clone.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
@@ -106,42 +105,42 @@ const emit = defineEmits<{
const inChannel = inject('inChannel', null);
-let note = $ref(deepClone(props.note));
+let note = ref(deepClone(props.note));
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
- let result = deepClone(note);
+ let result = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
- note = result;
+ note.value = result;
});
}
const replaceContent = () => {
- props.oldText ? note.text = props.oldText : undefined;
- note.createdAt = props.updatedAt;
+ props.oldText ? note.value.text = props.oldText : undefined;
+ note.value.createdAt = props.updatedAt;
};
replaceContent();
const isRenote = (
- note.renote != null &&
- note.text == null &&
- note.fileIds.length === 0 &&
- note.poll == null
+ note.value.renote != null &&
+ note.value.text == null &&
+ note.value.fileIds.length === 0 &&
+ note.value.poll == null
);
const el = shallowRef<HTMLElement>();
-let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note);
-const renoteUrl = appearNote.renote ? appearNote.renote.url : null;
-const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
+let appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note);
+const renoteUrl = appearNote.value.renote ? appearNote.value.renote.url : null;
+const renoteUri = appearNote.value.renote ? appearNote.value.renote.uri : null;
const showContent = ref(false);
const translation = ref(null);
const translating = ref(false);
-const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const urls = appearNote.value.text ? extractUrlFromMfm(mfm.parse(appearNote.value.text)).filter(u => u !== renoteUrl && u !== renoteUri) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
</script>
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index 095b24604a..6af63d1ec6 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { [$style.rootFirst]: first }]">
<div :class="[$style.label, { [$style.labelFirst]: first }]"><slot name="label"></slot></div>
+ <div :class="[$style.description]"><slot name="description"></slot></div>
<div :class="$style.main">
<slot></slot>
</div>
@@ -31,7 +32,7 @@ defineProps<{
.label {
font-weight: bold;
padding: 1.5em 0 0 0;
- margin: 0 0 16px 0;
+ margin: 0 0 8px 0;
&:empty {
display: none;
@@ -45,4 +46,10 @@ defineProps<{
.main {
margin: 1.5em 0 0 0;
}
+
+.description {
+ font-size: 0.85em;
+ color: var(--fgTransparentWeak);
+ margin: 0 0 8px 0;
+}
</style>
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index f65f8a78ff..af5daa10ff 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -21,7 +21,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
-import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 7689bba7bf..e2b59869a4 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -4,16 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" v-on:click.stop>
+<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu" @click.stop>
<slot></slot>
</a>
</template>
<script lang="ts" setup>
+import { computed } from 'vue';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
-import { popout as popout_ } from '@/scripts/popout.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@@ -28,7 +28,7 @@ const props = withDefaults(defineProps<{
const router = useRouter();
-const active = $computed(() => {
+const active = computed(() => {
if (props.activeClass == null) return false;
const resolved = router.resolve(props.to);
if (resolved == null) return false;
@@ -56,11 +56,11 @@ function onContextmenu(ev) {
action: () => {
router.push(props.to, 'forcePage');
},
- }, null, {
+ }, { type: 'divider' }, {
icon: 'ph-arrow-square-out ph-bold ph-lg',
text: i18n.ts.openInNewTab,
action: () => {
- window.open(props.to, '_blank');
+ window.open(props.to, '_blank', 'noopener');
},
}, {
icon: 'ph-link ph-bold ph-lg',
diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts
index 360bc88b4a..5ae45ec58f 100644
--- a/packages/frontend/src/components/global/MkAd.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts
@@ -4,11 +4,8 @@
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue';
-import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 3e092753a3..b3eb6d681f 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -96,7 +96,7 @@ const choseAd = (): Ad | null => {
};
const chosen = ref(choseAd());
-const shouldHide = $ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
+const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
function reduceFrequency(): void {
if (chosen.value == null) return;
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 01bf66fed5..4a876931c3 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -23,21 +23,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
- <img
- v-if="showDecoration && (decoration || user.avatarDecorations.length > 0)"
- :class="[$style.decoration]"
- :src="decoration?.url ?? user.avatarDecorations[0].url"
- :style="{
- rotate: getDecorationAngle(),
- scale: getDecorationScale(),
- }"
- alt=""
- >
+ <template v-if="showDecoration">
+ <img
+ v-for="decoration in decorations ?? user.avatarDecorations"
+ :class="[$style.decoration]"
+ :src="decoration.url"
+ :style="{
+ rotate: getDecorationAngle(decoration),
+ scale: getDecorationScale(decoration),
+ translate: getDecorationOffset(decoration),
+ }"
+ alt=""
+ >
+ </template>
</component>
</template>
<script lang="ts" setup>
-import { watch } from 'vue';
+import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
import MkA from './MkA.vue';
@@ -47,9 +50,9 @@ import { acct, userPage } from '@/filters/user.js';
import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
import { defaultStore } from '@/store.js';
-const animation = $ref(defaultStore.state.animation);
-const squareAvatars = $ref(defaultStore.state.squareAvatars);
-const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
+const animation = ref(defaultStore.state.animation);
+const squareAvatars = ref(defaultStore.state.squareAvatars);
+const useBlurEffect = ref(defaultStore.state.useBlurEffect);
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
@@ -57,19 +60,14 @@ const props = withDefaults(defineProps<{
link?: boolean;
preview?: boolean;
indicator?: boolean;
- decoration?: {
- url: string;
- angle?: number;
- flipH?: boolean;
- flipV?: boolean;
- };
+ decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[];
forceShowDecoration?: boolean;
}>(), {
target: null,
link: false,
preview: false,
indicator: false,
- decoration: undefined,
+ decorations: undefined,
forceShowDecoration: false,
});
@@ -79,11 +77,11 @@ const emit = defineEmits<{
const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
-const bound = $computed(() => props.link
+const bound = computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
-const url = $computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.enableDataSaverMode)
+const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar)
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl);
@@ -92,34 +90,26 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
-function getDecorationAngle() {
- let angle;
- if (props.decoration) {
- angle = props.decoration.angle ?? 0;
- } else if (props.user.avatarDecorations.length > 0) {
- angle = props.user.avatarDecorations[0].angle ?? 0;
- } else {
- angle = 0;
- }
+function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
}
-function getDecorationScale() {
- let scaleX;
- if (props.decoration) {
- scaleX = props.decoration.flipH ? -1 : 1;
- } else if (props.user.avatarDecorations.length > 0) {
- scaleX = props.user.avatarDecorations[0].flipH ? -1 : 1;
- } else {
- scaleX = 1;
- }
+function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const scaleX = decoration.flipH ? -1 : 1;
return scaleX === 1 ? undefined : `${scaleX} 1`;
}
-let color = $ref<string | undefined>();
+function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ const offsetX = decoration.offsetX ?? 0;
+ const offsetY = decoration.offsetY ?? 0;
+ return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
+}
+
+const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
- color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
+ color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
}, {
immediate: true,
});
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 10d7d93b01..e8732d1b16 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -19,12 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, inject } from 'vue';
+import { computed, inject, ref } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -71,7 +72,7 @@ const url = computed(() => {
});
const alt = computed(() => `:${customEmojiName.value}:`);
-let errored = $ref(url.value == null);
+const errored = ref(url.value == null);
function onClick(ev: MouseEvent) {
if (props.menu) {
@@ -90,6 +91,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(`:${props.name}:`);
+ sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index d5025edf82..b1d62db33c 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -16,6 +16,7 @@ import { defaultStore } from '@/store.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -56,6 +57,7 @@ function onClick(ev: MouseEvent) {
icon: 'ph-smiley ph-bold ph-lg',
action: () => {
react(props.emoji);
+ sound.play('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
new file mode 100644
index 0000000000..6d7ff4ca49
--- /dev/null
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -0,0 +1,53 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div ref="rootEl" :class="$style.root">
+ <div v-if="!showing" :class="$style.placeholder"></div>
+ <slot v-else></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue';
+
+const rootEl = shallowRef<HTMLDivElement>();
+const showing = ref(false);
+
+const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries.some((entry) => entry.isIntersecting)) {
+ showing.value = true;
+ }
+ },
+);
+
+onMounted(() => {
+ nextTick(() => {
+ observer.observe(rootEl.value!);
+ });
+});
+
+onActivated(() => {
+ nextTick(() => {
+ observer.observe(rootEl.value!);
+ });
+});
+
+onBeforeUnmount(() => {
+ observer.disconnect();
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ display: block;
+}
+
+.placeholder {
+ display: block;
+ min-height: 150px;
+}
+</style>
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index bd6a599a98..60d12fdcde 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -37,7 +37,7 @@ type MfmProps = {
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
- nyaize: boolean | 'respect';
+ nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
enableEmojiMenu?: boolean;
enableEmojiMenuReaction?: boolean;
@@ -110,26 +110,30 @@ export default function(props: MfmProps) {
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
- let style;
+ let style: string | undefined;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
- style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
- style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
- style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
- style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
break;
}
case 'spin': {
@@ -142,17 +146,20 @@ export default function(props: MfmProps) {
token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
const speed = validTime(token.props.args.speed) ?? '1.5s';
- style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
- style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
- style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
break;
}
case 'flip': {
@@ -202,7 +209,8 @@ export default function(props: MfmProps) {
}, genEl(token.children, scale));
}
const speed = validTime(token.props.args.speed) ?? '1s';
- style = `animation: mfm-rainbow ${speed} linear infinite;`;
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
break;
}
case 'sparkle': {
@@ -249,11 +257,17 @@ export default function(props: MfmProps) {
case 'ruby': {
if (token.children.length === 1) {
const child = token.children[0];
- const text = child.type === 'text' ? child.props.text : '';
+ let text = child.type === 'text' ? child.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = doNyaize(text);
+ }
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
} else {
const rt = token.children.at(-1)!;
- const text = rt.type === 'text' ? rt.props.text : '';
+ let text = rt.type === 'text' ? rt.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = doNyaize(text);
+ }
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
}
}
@@ -275,7 +289,7 @@ export default function(props: MfmProps) {
]);
}
}
- if (style == null) {
+ if (style === undefined) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index fd7aec5e5a..a36d9517cd 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -50,23 +50,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, ref, inject } from 'vue';
+import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js';
import { globalEvents } from '@/events.js';
import { injectPageMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { PageHeaderItem } from '@/types/page-header.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
- actions?: {
- text: string;
- icon: string;
- highlighted?: boolean;
- handler: (ev: MouseEvent) => void;
- }[];
+ actions?: PageHeaderItem[] | null;
thin?: boolean;
displayMyAvatar?: boolean;
displayBackButton?: boolean;
@@ -85,13 +81,13 @@ const metadata = injectPageMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
-let el = $shallowRef<HTMLElement | undefined>(undefined);
+const el = shallowRef<HTMLElement | undefined>(undefined);
const bg = ref<string | undefined>(undefined);
-let narrow = $ref(false);
-const hasTabs = $computed(() => props.tabs.length > 0);
-const hasActions = $computed(() => props.actions && props.actions.length > 0);
-const show = $computed(() => {
- return !hideTitle || hasTabs || hasActions;
+const narrow = ref(false);
+const hasTabs = computed(() => props.tabs.length > 0);
+const hasActions = computed(() => props.actions && props.actions.length > 0);
+const show = computed(() => {
+ return !hideTitle || hasTabs.value || hasActions.value;
});
const preventDrag = (ev: TouchEvent) => {
@@ -99,8 +95,8 @@ const preventDrag = (ev: TouchEvent) => {
};
const top = () => {
- if (el) {
- scrollToTop(el as HTMLElement, { behavior: 'smooth' });
+ if (el.value) {
+ scrollToTop(el.value as HTMLElement, { behavior: 'smooth' });
}
};
@@ -131,14 +127,14 @@ onMounted(() => {
calcBg();
globalEvents.on('themeChanged', calcBg);
- if (el && el.parentElement) {
- narrow = el.parentElement.offsetWidth < 500;
+ if (el.value && el.value.parentElement) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
- if (el && el.parentElement && document.body.contains(el as HTMLElement)) {
- narrow = el.parentElement.offsetWidth < 500;
+ if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
}
});
- ro.observe(el.parentElement as HTMLElement);
+ ro.observe(el.value.parentElement as HTMLElement);
}
});
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 8e9bff11d1..1d707af2d1 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -18,36 +18,36 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
-import { $$ } from 'vue/macros';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
+
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
-const rootEl = $shallowRef<HTMLElement>();
-const headerEl = $shallowRef<HTMLElement>();
-const footerEl = $shallowRef<HTMLElement>();
-const bodyEl = $shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
+const headerEl = shallowRef<HTMLElement>();
+const footerEl = shallowRef<HTMLElement>();
+const bodyEl = shallowRef<HTMLElement>();
-let headerHeight = $ref<string | undefined>();
-let childStickyTop = $ref(0);
+const headerHeight = ref<string | undefined>();
+const childStickyTop = ref(0);
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
-provide(CURRENT_STICKY_TOP, $$(childStickyTop));
+provide(CURRENT_STICKY_TOP, childStickyTop);
-let footerHeight = $ref<string | undefined>();
-let childStickyBottom = $ref(0);
+const footerHeight = ref<string | undefined>();
+const childStickyBottom = ref(0);
const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0));
-provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom));
+provide(CURRENT_STICKY_BOTTOM, childStickyBottom);
const calc = () => {
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
- if (headerEl != null) {
- childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
- headerHeight = headerEl.offsetHeight.toString();
+ if (headerEl.value != null) {
+ childStickyTop.value = parentStickyTop.value + headerEl.value.offsetHeight;
+ headerHeight.value = headerEl.value.offsetHeight.toString();
}
// コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
- if (footerEl != null) {
- childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight;
- footerHeight = footerEl.offsetHeight.toString();
+ if (footerEl.value != null) {
+ childStickyBottom.value = parentStickyBottom.value + footerEl.value.offsetHeight;
+ footerHeight.value = footerEl.value.offsetHeight.toString();
}
};
@@ -62,28 +62,28 @@ onMounted(() => {
watch([parentStickyTop, parentStickyBottom], calc);
- watch($$(childStickyTop), () => {
- bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
+ watch(childStickyTop, () => {
+ bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
}, {
immediate: true,
});
- watch($$(childStickyBottom), () => {
- bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`);
+ watch(childStickyBottom, () => {
+ bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
}, {
immediate: true,
});
- headerEl.style.position = 'sticky';
- headerEl.style.top = 'var(--stickyTop, 0)';
- headerEl.style.zIndex = '1000';
+ headerEl.value.style.position = 'sticky';
+ headerEl.value.style.top = 'var(--stickyTop, 0)';
+ headerEl.value.style.zIndex = '1000';
- footerEl.style.position = 'sticky';
- footerEl.style.bottom = 'var(--stickyBottom, 0)';
- footerEl.style.zIndex = '1000';
+ footerEl.value.style.position = 'sticky';
+ footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
+ footerEl.value.style.zIndex = '1000';
- observer.observe(headerEl);
- observer.observe(footerEl);
+ observer.observe(headerEl.value);
+ observer.observe(footerEl.value);
});
onUnmounted(() => {
@@ -91,6 +91,6 @@ onUnmounted(() => {
});
defineExpose({
- rootEl: $$(rootEl),
+ rootEl: rootEl,
});
</script>
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index f08d538fc0..e11db9dc31 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic';
-import { onMounted, onUnmounted } from 'vue';
+import { onMounted, onUnmounted, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@/scripts/intl-const.js';
@@ -28,35 +28,48 @@ const props = withDefaults(defineProps<{
mode: 'relative',
});
-const _time = props.time == null ? NaN :
- typeof props.time === 'number' ? props.time :
- (props.time instanceof Date ? props.time : new Date(props.time)).getTime();
+function getDateSafe(n: Date | string | number) {
+ try {
+ if (n instanceof Date) {
+ return n;
+ }
+ return new Date(n);
+ } catch (err) {
+ return {
+ getTime: () => NaN,
+ };
+ }
+}
+
+// eslint-disable-next-line vue/no-setup-props-destructure
+const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
-let now = $ref((props.origin ?? new Date()).getTime());
-const ago = $computed(() => (now - _time) / 1000/*ms*/);
+// eslint-disable-next-line vue/no-setup-props-destructure
+const now = ref((props.origin ?? new Date()).getTime());
+const ago = computed(() => (now.value - _time) / 1000/*ms*/);
-const relative = $computed<string>(() => {
+const relative = computed<string>(() => {
if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
if (invalid) return i18n.ts._ago.invalid;
return (
- ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago / 31536000).toString() }) :
- ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago / 2592000).toString() }) :
- ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago / 604800).toString() }) :
- ago >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago / 86400).toString() }) :
- ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
- ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
- ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
- ago >= -3 ? i18n.ts._ago.justNow :
- ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) :
- ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) :
- ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) :
- ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) :
- ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) :
- ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) :
- i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() })
+ ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
+ ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
+ ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
+ ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
+ ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
+ ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
+ ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
+ ago.value >= -3 ? i18n.ts._ago.justNow :
+ ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
+ ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
+ ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
+ ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
+ ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
+ ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
+ i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
);
});
@@ -64,8 +77,8 @@ let tickId: number;
let currentInterval: number;
function tick() {
- now = (new Date()).getTime();
- const nextInterval = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
+ now.value = (new Date()).getTime();
+ const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
if (currentInterval !== nextInterval) {
if (tickId) window.clearInterval(tickId);
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index d29c720278..667a113432 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component
- :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
+ :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
index 8c24a4819f..01455e492d 100644
--- a/packages/frontend/src/components/global/MkUserName.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
@@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
-import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes';
import MkUserName from './MkUserName.vue';
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 99f42f4fcb..9da8f8c379 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, onBeforeUnmount, provide } from 'vue';
+import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
import { Resolved, Router } from '@/nirax';
import { defaultStore } from '@/store.js';
@@ -46,16 +46,16 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
}
const current = resolveNested(router.current)!;
-let currentPageComponent = $shallowRef(current.route.component);
-let currentPageProps = $ref(current.props);
-let key = $ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
+const currentPageComponent = shallowRef(current.route.component);
+const currentPageProps = ref(current.props);
+const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved);
if (current == null) return;
- currentPageComponent = current.route.component;
- currentPageProps = current.props;
- key = current.route.path + JSON.stringify(Object.fromEntries(current.props));
+ currentPageComponent.value = current.route.component;
+ currentPageProps.value = current.props;
+ key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
}
router.addListener('change', onChange);
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index c740d181f9..a3e13c3a50 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -25,6 +25,7 @@ import MkPageHeader from './global/MkPageHeader.vue';
import MkSpacer from './global/MkSpacer.vue';
import MkFooterSpacer from './global/MkFooterSpacer.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
+import MkLazy from './global/MkLazy.vue';
export default function(app: App) {
for (const [key, value] of Object.entries(components)) {
@@ -53,6 +54,7 @@ export const components = {
MkSpacer: MkSpacer,
MkFooterSpacer: MkFooterSpacer,
MkStickyContainer: MkStickyContainer,
+ MkLazy: MkLazy,
};
declare module '@vue/runtime-core' {
@@ -77,5 +79,6 @@ declare module '@vue/runtime-core' {
MkSpacer: typeof MkSpacer;
MkFooterSpacer: typeof MkFooterSpacer;
MkStickyContainer: typeof MkStickyContainer;
+ MkLazy: typeof MkLazy;
}
}
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index 6aa2c1c0b7..892522d4b5 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -16,7 +16,6 @@ import * as mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js';
import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
index ab37ca69ad..94ca7bdf04 100644
--- a/packages/frontend/src/components/page/page.vue
+++ b/packages/frontend/src/components/page/page.vue
@@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, nextTick } from 'vue';
import * as Misskey from 'misskey-js';
import XBlock from './page.block.vue';