summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-02-25 20:04:48 +0100
committerGitHub <noreply@github.com>2023-02-25 20:04:48 +0100
commitb468330ed944cd2aefb93183786855e990bd3df3 (patch)
treeaae515a3d90bc6646854ea718c054540b2b654e9 /packages/frontend/src/components
parentAdd test (diff)
parentrefactor(frontend): fix eslint error (#10084) (diff)
downloadmisskey-b468330ed944cd2aefb93183786855e990bd3df3.tar.gz
misskey-b468330ed944cd2aefb93183786855e990bd3df3.tar.bz2
misskey-b468330ed944cd2aefb93183786855e990bd3df3.zip
Merge branch 'develop' into mkusername-empty
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkAbuseReportWindow.vue2
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue2
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue2
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue3
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue10
-rw-r--r--packages/frontend/src/components/MkCwButton.vue3
-rw-r--r--packages/frontend/src/components/MkDialog.vue39
-rw-r--r--packages/frontend/src/components/MkFolder.vue35
-rw-r--r--packages/frontend/src/components/MkInput.vue6
-rw-r--r--packages/frontend/src/components/MkMediaList.vue8
-rw-r--r--packages/frontend/src/components/MkMenu.child.vue35
-rw-r--r--packages/frontend/src/components/MkMenu.vue28
-rw-r--r--packages/frontend/src/components/MkModal.vue23
-rw-r--r--packages/frontend/src/components/MkNote.vue55
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue8
-rw-r--r--packages/frontend/src/components/MkNoteHeader.vue2
-rw-r--r--packages/frontend/src/components/MkNotificationSettingWindow.vue30
-rw-r--r--packages/frontend/src/components/MkNotifications.vue2
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue11
-rw-r--r--packages/frontend/src/components/MkPagination.vue39
-rw-r--r--packages/frontend/src/components/MkPostForm.vue16
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue19
-rw-r--r--packages/frontend/src/components/MkSelect.vue4
-rw-r--r--packages/frontend/src/components/MkSignin.vue6
-rw-r--r--packages/frontend/src/components/MkTimeline.vue6
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue29
-rw-r--r--packages/frontend/src/components/MkUserList.vue11
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue5
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue4
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue6
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue50
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue13
-rw-r--r--packages/frontend/src/components/global/MkTime.vue25
-rw-r--r--packages/frontend/src/components/mfm.ts2
34 files changed, 372 insertions, 167 deletions
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index a76a1e0f54..9f2bf99338 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -43,7 +43,7 @@ const emit = defineEmits<{
}>();
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
-const comment = ref(props.initialComment || '');
+const comment = ref(props.initialComment ?? '');
function send() {
os.apiWithDialog('users/report-abuse', {
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 7e5432434f..663c57623d 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -209,7 +209,7 @@ function exec() {
}
} else if (props.type === 'hashtag') {
if (!props.q || props.q === '') {
- hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
+ hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]');
fetching.value = false;
} else {
const cacheKey = `autocomplete:hashtag:${props.q}`;
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 8db2e54e88..c72cc2ab1b 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -69,7 +69,7 @@ const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown
if (loaded) {
available.value = true;
} else {
- (document.getElementById(scriptId.value) || document.head.appendChild(Object.assign(document.createElement('script'), {
+ (document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
async: true,
id: scriptId.value,
src: src.value,
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 2283e652db..da6439fd2c 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -22,9 +22,6 @@ import * as game from '@/scripts/clicker-game';
import number from '@/filters/number';
import { claimAchievement } from '@/scripts/achievements';
-defineProps<{
-}>();
-
const saveData = game.saveData;
const cookies = computed(() => saveData.value?.cookies);
let cps = $ref(0);
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index f0ea984c4e..21cccaabde 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -32,6 +32,8 @@ let rootEl = $shallowRef<HTMLDivElement>();
let zIndex = $ref<number>(os.claimZIndex('high'));
+const SCROLLBAR_THICKNESS = 16;
+
onMounted(() => {
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
@@ -39,12 +41,12 @@ onMounted(() => {
const width = rootEl.offsetWidth;
const height = rootEl.offsetHeight;
- if (left + width - window.pageXOffset > window.innerWidth) {
- left = window.innerWidth - width + window.pageXOffset;
+ if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
+ left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
}
- if (top + height - window.pageYOffset > window.innerHeight) {
- top = window.innerHeight - height + window.pageYOffset;
+ if (top + height - window.pageYOffset >= (window.innerHeight - SCROLLBAR_THICKNESS)) {
+ top = (window.innerHeight - SCROLLBAR_THICKNESS) - height + window.pageYOffset;
}
if (top < 0) {
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index e0885f5550..7d5579040a 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -7,7 +7,6 @@
<script lang="ts" setup>
import { computed } from 'vue';
-import { length } from 'stringz';
import * as misskey from 'misskey-js';
import { concat } from '@/scripts/array';
import { i18n } from '@/i18n';
@@ -23,7 +22,7 @@ const emit = defineEmits<{
const label = computed(() => {
return concat([
- props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
+ 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] : [],
] as string[][]).join(' / ');
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 9690353432..863ea702cd 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -14,8 +14,12 @@
</div>
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
- <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
+ <MkInput v-if="input" 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="ti ti-lock"></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' })" />
+ </template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
@@ -28,7 +32,7 @@
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
- <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
+ <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" :disabled="okButtonDisabled" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
</div>
<div v-if="actions" :class="$style.buttons">
@@ -47,9 +51,12 @@ import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n';
type Input = {
- type: HTMLInputElement['type'];
+ type: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
placeholder?: string | null;
- default: any | null;
+ autocomplete?: string;
+ default: string | number | null;
+ minLength?: number;
+ maxLength?: number;
};
type Select = {
@@ -98,8 +105,28 @@ const emit = defineEmits<{
const modal = shallowRef<InstanceType<typeof MkModal>>();
-const inputValue = ref(props.input?.default || null);
-const selectedValue = ref(props.select?.default || null);
+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>(() => {
+ if (props.input) {
+ if (props.input.minLength) {
+ if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
+ disabledReason = 'charactersBelow';
+ return true;
+ }
+ }
+ if (props.input.maxLength) {
+ if (inputValue.value && (inputValue.value as string).length > props.input.maxLength) {
+ disabledReason = 'charactersExceeded';
+ return true;
+ }
+ }
+ }
+
+ return false;
+});
function done(canceled: boolean, result?) {
emit('done', { canceled, result });
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index a1d7210d7e..b97e36cd5f 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -1,13 +1,20 @@
<template>
<div ref="rootEl" :class="[$style.root, { [$style.opened]: opened }]">
<div :class="$style.header" class="_button" @click="toggle">
- <span :class="$style.headerIcon"><slot name="icon"></slot></span>
- <span :class="$style.headerText"><slot name="label"></slot></span>
- <span :class="$style.headerRight">
+ <div :class="$style.headerIcon"><slot name="icon"></slot></div>
+ <div :class="$style.headerText">
+ <div :class="$style.headerTextMain">
+ <slot name="label"></slot>
+ </div>
+ <div :class="$style.headerTextSub">
+ <slot name="caption"></slot>
+ </div>
+ </div>
+ <div :class="$style.headerRight">
<span :class="$style.headerRightText"><slot name="suffix"></slot></span>
<i v-if="opened" class="ti ti-chevron-up icon"></i>
<i v-else class="ti ti-chevron-down icon"></i>
- </span>
+ </div>
</div>
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
<Transition
@@ -139,6 +146,17 @@ onMounted(() => {
}
}
+.headerUpper {
+ display: flex;
+ align-items: center;
+}
+
+.headerLower {
+ color: var(--fgTransparentWeak);
+ font-size: .85em;
+ padding-left: 4px;
+}
+
.headerIcon {
margin-right: 0.75em;
flex-shrink: 0;
@@ -161,6 +179,15 @@ onMounted(() => {
padding-right: 12px;
}
+.headerTextMain {
+
+}
+
+.headerTextSub {
+ color: var(--fgTransparentWeak);
+ font-size: .85em;
+}
+
.headerRight {
margin-left: auto;
opacity: 0.7;
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index da6177c2f9..3e3d7354c1 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -23,7 +23,7 @@
@input="onInput"
>
<datalist v-if="datalist" :id="id">
- <option v-for="data in datalist" :value="data"/>
+ <option v-for="data in datalist" :key="data" :value="data"/>
</datalist>
<div ref="suffixEl" class="suffix"><slot name="suffix"></slot></div>
</div>
@@ -41,7 +41,7 @@ import { useInterval } from '@/scripts/use-interval';
import { i18n } from '@/i18n';
const props = defineProps<{
- modelValue: string | number;
+ modelValue: string | number | null;
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
required?: boolean;
readonly?: boolean;
@@ -49,7 +49,7 @@ const props = defineProps<{
pattern?: string;
placeholder?: string;
autofocus?: boolean;
- autocomplete?: boolean;
+ autocomplete?: string;
spellcheck?: boolean;
step?: any;
datalist?: string[];
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index e957d8f56c..a12bb78e35 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -45,8 +45,8 @@ onMounted(() => {
src: media.url,
w: media.properties.width,
h: media.properties.height,
- alt: media.comment || media.name,
- comment: media.comment || media.name,
+ alt: media.comment ?? media.name,
+ comment: media.comment ?? media.name,
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
@@ -90,8 +90,8 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl;
- itemData.alt = file.comment || file.name;
- itemData.comment = file.comment || file.name;
+ itemData.alt = file.comment ?? file.name;
+ itemData.comment = file.comment ?? file.name;
itemData.thumbCropped = true;
});
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index cdd9d96b96..e0935efbe7 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -1,11 +1,11 @@
<template>
-<div ref="el" class="sfhdhdhr">
- <MkMenu ref="menu" :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
+<div ref="el" :class="$style.root">
+ <MkMenu :items="items" :align="align" :width="width" :as-drawer="false" @close="onChildClosed"/>
</div>
</template>
<script lang="ts" setup>
-import { nextTick, onMounted, shallowRef, watch } from 'vue';
+import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import MkMenu from './MkMenu.vue';
import { MenuItem } from '@/types/menu';
@@ -25,11 +25,21 @@ const emit = defineEmits<{
const el = shallowRef<HTMLElement>();
const align = 'left';
+const SCROLLBAR_THICKNESS = 16;
+
function setPosition() {
const rootRect = props.rootElement.getBoundingClientRect();
- const rect = props.targetElement.getBoundingClientRect();
- const left = props.targetElement.offsetWidth;
- const top = (rect.top - rootRect.top) - 8;
+ const parentRect = props.targetElement.getBoundingClientRect();
+ const myRect = el.value.getBoundingClientRect();
+
+ let left = props.targetElement.offsetWidth;
+ let top = (parentRect.top - rootRect.top) - 8;
+ if (rootRect.left + left + myRect.width >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
+ left = -myRect.width;
+ }
+ if (rootRect.top + top + myRect.height >= (window.innerHeight - SCROLLBAR_THICKNESS)) {
+ top = top - ((rootRect.top + top + myRect.height) - (window.innerHeight - SCROLLBAR_THICKNESS));
+ }
el.value.style.left = left + 'px';
el.value.style.top = top + 'px';
}
@@ -46,13 +56,22 @@ watch(() => props.targetElement, () => {
setPosition();
});
+const ro = new ResizeObserver((entries, observer) => {
+ setPosition();
+});
+
onMounted(() => {
+ ro.observe(el.value);
setPosition();
nextTick(() => {
setPosition();
});
});
+onUnmounted(() => {
+ ro.disconnect();
+});
+
defineExpose({
checkHit: (ev: MouseEvent) => {
return (ev.target === el.value || el.value.contains(ev.target));
@@ -60,8 +79,8 @@ defineExpose({
});
</script>
-<style lang="scss" scoped>
-.sfhdhdhr {
+<style lang="scss" module>
+.root {
position: absolute;
}
</style>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 52aba58455..09d530c4ea 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -56,7 +56,7 @@
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, watch } from 'vue';
+import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus';
import MkSwitch from '@/components/MkSwitch.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
@@ -111,11 +111,11 @@ watch(() => props.items, () => {
immediate: true,
});
-let childMenu = $ref<MenuItem[] | null>();
+let childMenu = ref<MenuItem[] | null>();
let childTarget = $shallowRef<HTMLElement | null>();
function closeChild() {
- childMenu = null;
+ childMenu.value = null;
childShowingItem = null;
}
@@ -140,13 +140,31 @@ function onItemMouseLeave(item) {
if (childCloseTimer) window.clearTimeout(childCloseTimer);
}
+let childrenCache = new WeakMap();
async function showChildren(item: MenuItem, ev: MouseEvent) {
+ const children = ref([]);
+ if (childrenCache.has(item)) {
+ children.value = childrenCache.get(item);
+ } else {
+ if (typeof item.children === 'function') {
+ children.value = [{
+ type: 'pending',
+ }];
+ item.children().then(x => {
+ children.value = x;
+ childrenCache.set(item, x);
+ });
+ } else {
+ children.value = item.children;
+ }
+ }
+
if (props.asDrawer) {
- os.popupMenu(item.children, ev.currentTarget ?? ev.target);
+ os.popupMenu(children, ev.currentTarget ?? ev.target);
close();
} else {
childTarget = ev.currentTarget ?? ev.target;
- childMenu = item.children;
+ childMenu = children;
childShowingItem = item;
}
}
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index eba0f5847d..dbad02fb7e 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -125,7 +125,7 @@ function onBgClick() {
}
if (type === 'drawer') {
- maxHeight = window.innerHeight / 1.5;
+ maxHeight = (window.innerHeight - SCROLLBAR_THICKNESS) / 1.5;
}
const keymap = {
@@ -133,6 +133,7 @@ const keymap = {
};
const MARGIN = 16;
+const SCROLLBAR_THICKNESS = 16;
const align = () => {
if (props.src == null) return;
@@ -170,15 +171,15 @@ const align = () => {
if (fixed) {
// 画面から横にはみ出る場合
- if (left + width > window.innerWidth) {
- left = window.innerWidth - width;
+ if (left + width > (window.innerWidth - SCROLLBAR_THICKNESS)) {
+ left = (window.innerWidth - SCROLLBAR_THICKNESS) - width;
}
- const underSpace = (window.innerHeight - MARGIN) - top;
+ const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - top;
const upperSpace = (srcRect.top - MARGIN);
// 画面から縦にはみ出る場合
- if (top + height > (window.innerHeight - MARGIN)) {
+ if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
maxHeight = underSpace;
@@ -187,22 +188,22 @@ const align = () => {
top = (upperSpace + MARGIN) - height;
}
} else {
- top = (window.innerHeight - MARGIN) - height;
+ top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height;
}
} else {
maxHeight = underSpace;
}
} else {
// 画面から横にはみ出る場合
- if (left + width - window.pageXOffset > window.innerWidth) {
- left = window.innerWidth - width + window.pageXOffset - 1;
+ if (left + width - window.pageXOffset > (window.innerWidth - SCROLLBAR_THICKNESS)) {
+ left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset - 1;
}
- const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
+ const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.pageYOffset);
const upperSpace = (srcRect.top - MARGIN);
// 画面から縦にはみ出る場合
- if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
+ if (top + height - window.pageYOffset > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
if (props.noOverlap && props.anchor.x === 'center') {
if (underSpace >= (upperSpace / 3)) {
maxHeight = underSpace;
@@ -211,7 +212,7 @@ const align = () => {
top = window.pageYOffset + ((upperSpace + MARGIN) - height);
}
} else {
- top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
+ top = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - height + window.pageYOffset - 1;
}
} else {
maxHeight = underSpace;
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index f4c044e0bd..1040dac12e 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -31,7 +31,7 @@
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
- <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
+ <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
</div>
</div>
<div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget">
@@ -155,7 +155,6 @@ import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
import { getNoteSummary } from '@/scripts/get-note-summary';
-import { shownNoteIds } from '@/os';
import { MenuItem } from '@/types/menu';
const props = defineProps<{
@@ -195,6 +194,8 @@ const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
const isLong = (appearNote.cw == null && appearNote.text != null && (
+ (appearNote.text.includes('$[x3')) ||
+ (appearNote.text.includes('$[x4')) ||
(appearNote.text.split('\n').length > 9) ||
(appearNote.text.length > 500) ||
(appearNote.files.length >= 5) ||
@@ -207,9 +208,7 @@ 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.userId === $i.id);
-let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId)) || shownNoteIds.has(appearNote.id)));
-
-shownNoteIds.add(appearNote.id);
+let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId)) || (appearNote.myReaction != null)));
const keymap = {
'r': () => reply(true),
@@ -256,7 +255,7 @@ function renote(viaKeyboard = false) {
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
- os.api('notes/create', {
+ os.apiWithDialog('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
});
@@ -277,7 +276,7 @@ function renote(viaKeyboard = false) {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
- os.api('notes/create', {
+ os.apiWithDialog('notes/create', {
renoteId: appearNote.id,
});
},
@@ -674,9 +673,17 @@ function showReactions(): void {
opacity: 0.7;
}
-@container (max-width: 500px) {
+@container (max-width: 580px) {
.root {
- font-size: 0.9em;
+ font-size: 0.95em;
+ }
+
+ .renote {
+ padding: 12px 26px 0 26px;
+ }
+
+ .article {
+ padding: 24px 26px 14px;
}
.avatar {
@@ -685,7 +692,21 @@ function showReactions(): void {
}
}
-@container (max-width: 450px) {
+@container (max-width: 500px) {
+ .root {
+ font-size: 0.9em;
+ }
+
+ .renote {
+ padding: 10px 22px 0 22px;
+ }
+
+ .article {
+ padding: 20px 22px 12px;
+ }
+}
+
+@container (max-width: 480px) {
.renote {
padding: 8px 16px 0 16px;
}
@@ -702,7 +723,9 @@ function showReactions(): void {
.article {
padding: 14px 16px 9px;
}
+}
+@container (max-width: 450px) {
.avatar {
margin: 0 10px 8px 0;
width: 46px;
@@ -711,7 +734,7 @@ function showReactions(): void {
}
}
-@container (max-width: 350px) {
+@container (max-width: 400px) {
.footerButton {
&:not(:last-child) {
margin-right: 18px;
@@ -719,6 +742,14 @@ function showReactions(): void {
}
}
+@container (max-width: 350px) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
+ }
+}
+
@container (max-width: 300px) {
.avatar {
width: 44px;
@@ -727,7 +758,7 @@ function showReactions(): void {
.footerButton {
&:not(:last-child) {
- margin-right: 12px;
+ margin-right: 8px;
}
}
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 82e0f3e689..2eebe999a5 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -30,7 +30,7 @@
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
- <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
+ <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
</div>
</div>
<article class="article" @contextmenu.stop="onContextmenu">
@@ -48,7 +48,7 @@
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
- <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
+ <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
</div>
</div>
<div class="username"><MkAcct :user="appearNote.user"/></div>
@@ -250,7 +250,7 @@ function renote(viaKeyboard = false) {
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
- os.api('notes/create', {
+ os.apiWithDialog('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
});
@@ -271,7 +271,7 @@ function renote(viaKeyboard = false) {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
- os.api('notes/create', {
+ os.apiWithDialog('notes/create', {
renoteId: appearNote.id,
});
},
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 32998e1a70..ffd9a20ef7 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -17,7 +17,7 @@
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
- <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
+ <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
</div>
</header>
</template>
diff --git a/packages/frontend/src/components/MkNotificationSettingWindow.vue b/packages/frontend/src/components/MkNotificationSettingWindow.vue
index e303403872..f6d0e5681d 100644
--- a/packages/frontend/src/components/MkNotificationSettingWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSettingWindow.vue
@@ -6,7 +6,7 @@
:with-ok-button="true"
:ok-button-disabled="false"
@ok="ok()"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.notificationSetting }}</template>
@@ -25,7 +25,7 @@
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
- <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
+ <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
</template>
</div>
</MkSpacer>
@@ -33,14 +33,16 @@
</template>
<script lang="ts" setup>
-import { } from 'vue';
-import { notificationTypes } from 'misskey-js';
+import { ref, Ref } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
+import { notificationTypes } from '@/const';
import { i18n } from '@/i18n';
+type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>
+
const emit = defineEmits<{
(ev: 'done', v: { includingTypes: string[] | null }): void,
(ev: 'closed'): void,
@@ -54,39 +56,35 @@ const props = withDefaults(defineProps<{
showGlobalToggle: true,
});
-let includingTypes = $computed(() => props.includingTypes || []);
+let includingTypes = $computed(() => props.includingTypes?.filter(x => notificationTypes.includes(x)) ?? []);
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
-let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
+const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(includingTypes.includes(t)) }), {} as any);
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
-for (const ntype of notificationTypes) {
- typesMap[ntype] = includingTypes.includes(ntype);
-}
-
function ok() {
if (useGlobalSetting) {
emit('done', { includingTypes: null });
} else {
emit('done', {
includingTypes: (Object.keys(typesMap) as typeof notificationTypes[number][])
- .filter(type => typesMap[type]),
+ .filter(type => typesMap[type].value),
});
}
- dialog.close();
+ if (dialog) dialog.close();
}
function disableAll() {
- for (const type in typesMap) {
- typesMap[type as typeof notificationTypes[number]] = false;
+ for (const type of notificationTypes) {
+ typesMap[type].value = false;
}
}
function enableAll() {
- for (const type in typesMap) {
- typesMap[type as typeof notificationTypes[number]] = true;
+ for (const type of notificationTypes) {
+ typesMap[type].value = true;
}
}
</script>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 37ce7635a3..93b1c37055 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -18,7 +18,6 @@
<script lang="ts" setup>
import { onUnmounted, onMounted, computed, shallowRef } from 'vue';
-import { notificationTypes } from 'misskey-js';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
@@ -26,6 +25,7 @@ import XNote from '@/components/MkNote.vue';
import { stream } from '@/stream';
import { $i } from '@/account';
import { i18n } from '@/i18n';
+import { notificationTypes } from '@/const';
const props = defineProps<{
includeTypes?: typeof notificationTypes[number][];
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 98115dd424..02ce58451d 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -18,7 +18,7 @@
</template>
<div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
- <RouterView :router="router"/>
+ <RouterView :key="reloadCount" :router="router"/>
</div>
</MkWindow>
</template>
@@ -67,6 +67,10 @@ const buttonsLeft = $computed(() => {
});
const buttonsRight = $computed(() => {
const buttons = [{
+ icon: 'ti ti-reload',
+ title: i18n.ts.reload,
+ onClick: reload,
+ }, {
icon: 'ti ti-player-eject',
title: i18n.ts.showInPage,
onClick: expand,
@@ -74,6 +78,7 @@ const buttonsRight = $computed(() => {
return buttons;
});
+let reloadCount = $ref(0);
router.addListener('push', ctx => {
history.push({ path: ctx.path, key: ctx.key });
@@ -115,6 +120,10 @@ function back() {
router.replace(history[history.length - 1].path, history[history.length - 1].key);
}
+function reload() {
+ reloadCount++;
+}
+
function close() {
windowEl.close();
}
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 224a42cdc2..378d0ac020 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -42,6 +42,7 @@ import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, o
import * as misskey from 'misskey-js';
import * as os from '@/os';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll';
+import { useDocumentVisibility } from '@/scripts/use-document-visibility';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store';
import { MisskeyEntity } from '@/types/date-separated-list';
@@ -104,9 +105,15 @@ const {
enableInfiniteScroll,
} = defaultStore.reactiveState;
-const contentEl = $computed(() => props.pagination.pageEl || rootEl);
+const contentEl = $computed(() => props.pagination.pageEl ?? rootEl);
const scrollableElement = $computed(() => getScrollContainer(contentEl));
+const visibility = useDocumentVisibility();
+
+let isPausingUpdate = false;
+let timerForSetPause: number | null = null;
+const BACKGROUND_PAUSE_WAIT_SEC = 10;
+
// 先頭が表示されているかどうかを検出
// https://qiita.com/mkataigi/items/0154aefd2223ce23398e
let scrollObserver = $ref<IntersectionObserver>();
@@ -279,6 +286,28 @@ const fetchMoreAhead = async (): Promise<void> => {
});
};
+const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
+
+watch(visibility, () => {
+ if (visibility.value === 'hidden') {
+ timerForSetPause = window.setTimeout(() => {
+ isPausingUpdate = true;
+ timerForSetPause = null;
+ },
+ BACKGROUND_PAUSE_WAIT_SEC * 1000);
+ } else { // 'visible'
+ if (timerForSetPause) {
+ clearTimeout(timerForSetPause);
+ timerForSetPause = null;
+ } else {
+ isPausingUpdate = false;
+ if (isTop()) {
+ executeQueue();
+ }
+ }
+ }
+});
+
const prepend = (item: MisskeyEntity): void => {
// 初回表示時はunshiftだけでOK
if (!rootEl) {
@@ -286,9 +315,7 @@ const prepend = (item: MisskeyEntity): void => {
return;
}
- const isTop = isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl, TOLERANCE);
-
- if (isTop) unshiftItems([item]);
+ if (isTop() && !isPausingUpdate) unshiftItems([item]);
else prependQueue(item);
};
@@ -357,6 +384,10 @@ onMounted(() => {
});
onBeforeUnmount(() => {
+ if (timerForSetPause) {
+ clearTimeout(timerForSetPause);
+ timerForSetPause = null;
+ }
scrollObserver.disconnect();
});
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 44462f8ff2..f73eab5b86 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -45,6 +45,7 @@
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div>
</div>
+ <MkInfo v-if="localOnly && channel == null" warn :class="$style.disableFederationWarn">{{ i18n.ts.disableFederationWarn }}</MkInfo>
<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">
<textarea ref="textareaEl" v-model="text" :class="[$style.text, { [$style.withCw]: useCw }]" :disabled="posting || posted" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
@@ -73,7 +74,6 @@ import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
-import { length } from 'stringz';
import { toASCII } from 'punycode/';
import * as Acct from 'misskey-js/built/acct';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -155,7 +155,7 @@ 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 recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]'));
let imeText = $ref('');
const draftKey = $computed((): string => {
@@ -201,7 +201,7 @@ const submitText = $computed((): string => {
});
const textLength = $computed((): number => {
- return length((text + imeText).trim());
+ return (text + imeText).trim().length;
});
const maxTextLength = $computed((): number => {
@@ -534,7 +534,7 @@ function onDrop(ev): void {
}
function saveDraft() {
- const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
+ const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
draftData[draftKey] = {
updatedAt: new Date(),
@@ -643,7 +643,7 @@ async function post(ev?: MouseEvent) {
emit('posted');
if (postData.text && postData.text !== '') {
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
- const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
+ const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
}
posting = false;
@@ -747,7 +747,7 @@ onMounted(() => {
nextTick(() => {
// 書きかけの投稿を復元
if (!props.instant && !props.mention && !props.specified) {
- const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
+ const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey];
if (draft) {
text = draft.data.text;
useCw = draft.data.useCw;
@@ -942,6 +942,10 @@ defineExpose({
background: var(--X4);
}
+.disableFederationWarn {
+ margin: 0 20px 16px 20px;
+}
+
.hasNotSpecifiedMentions {
margin: 0 20px 16px 20px;
}
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 8c1d7af190..2f5866f340 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -1,10 +1,15 @@
<template>
-<MkA v-adaptive-bg :to="`/admin/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
+<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<div :class="$style.title">
<span :class="$style.icon">
- <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
- <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
- <i v-else class="ti ti-user" style="opacity: 0.7;"></i>
+ <template v-if="role.iconUrl">
+ <img :class="$style.badge" :src="role.iconUrl"/>
+ </template>
+ <template v-else>
+ <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
+ <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
+ <i v-else class="ti ti-user" style="opacity: 0.7;"></i>
+ </template>
</span>
<span :class="$style.name">{{ role.name }}</span>
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
@@ -20,6 +25,7 @@ import { i18n } from '@/i18n';
const props = defineProps<{
role: any;
+ forModeration: boolean;
}>();
</script>
@@ -38,6 +44,11 @@ const props = defineProps<{
margin-right: 8px;
}
+.badge {
+ height: 1.3em;
+ vertical-align: -20%;
+}
+
.name {
font-weight: bold;
}
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index cb64b1e484..2de890186a 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -34,7 +34,7 @@ import { useInterval } from '@/scripts/use-interval';
import { i18n } from '@/i18n';
const props = defineProps<{
- modelValue: string;
+ modelValue: string | null;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@@ -48,7 +48,7 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void;
- (ev: 'update:modelValue', value: string): void;
+ (ev: 'update:modelValue', value: string | null): void;
}>();
const slots = useSlots();
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index ae4f38e56c..ffc5e82b56 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -10,7 +10,7 @@
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
+ <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password" :with-password-toggle="true" required data-cy-signin-password>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
@@ -28,11 +28,11 @@
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
- <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
+ <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :with-password-toggle="true" required>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
</MkInput>
- <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" :spellcheck="false" required>
+ <MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="one-time-code" :spellcheck="false" required>
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="ti ti-123"></i></template>
</MkInput>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 1ba48bf77d..87f7c61a92 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -1,10 +1,10 @@
<template>
-<XNotes ref="tlComponent" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
+<MkNotes ref="tlComponent" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
</template>
<script lang="ts" setup>
import { computed, provide, onUnmounted } from 'vue';
-import XNotes from '@/components/MkNotes.vue';
+import MkNotes from '@/components/MkNotes.vue';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
@@ -24,7 +24,7 @@ const emit = defineEmits<{
provide('inChannel', computed(() => props.src === 'channel'));
-const tlComponent: InstanceType<typeof XNotes> = $ref();
+const tlComponent: InstanceType<typeof MkNotes> = $ref();
const prepend = note => {
tlComponent.pagingComponent?.prepend(note);
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index b97b7cf07b..5381ecbfa5 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -1,12 +1,25 @@
<template>
-<div v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
- <button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
- <iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
- <span v-else>invalid url</span>
-</div>
-<div v-else-if="tweetId && tweetExpanded" ref="twitter" :class="$style.twitter">
- <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
-</div>
+<template v-if="playerEnabled">
+ <div :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
+ <iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
+ <span v-else>invalid url</span>
+ </div>
+ <div :class="$style.action">
+ <MkButton :small="true" inline @click="playerEnabled = false">
+ <i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }}
+ </MkButton>
+ </div>
+</template>
+<template v-else-if="tweetId && tweetExpanded">
+ <div ref="twitter" :class="$style.twitter">
+ <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+ </div>
+ <div :class="$style.action">
+ <MkButton :small="true" inline @click="tweetExpanded = false">
+ <i class="ti ti-x"></i> {{ i18n.ts.close }}
+ </MkButton>
+ </div>
+</template>
<div v-else :class="$style.urlPreview">
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index dd683fcc23..51eb426e97 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -7,9 +7,9 @@
</div>
</template>
- <template #default="{ items: users }">
+ <template #default="{ items }">
<div class="efvhhmdq">
- <MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
+ <MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/>
</div>
</template>
</MkPagination>
@@ -20,10 +20,13 @@ import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
pagination: Paging;
noGap?: boolean;
-}>();
+ extractor?: (item: any) => any;
+}>(), {
+ extractor: (item) => item,
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 981ae56e6c..dc78bbf42d 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -16,7 +16,7 @@
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model="host" @update:model-value="search">
+ <MkInput v-model="host" :datalist="[hostname]" @update:model-value="search">
<template #label>{{ i18n.ts.host }}</template>
<template #prefix>@</template>
</MkInput>
@@ -61,6 +61,7 @@ import * as os from '@/os';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { $i } from '@/account';
+import { hostname } from '@/config';
const emit = defineEmits<{
(ev: 'ok', selected: misskey.entities.UserDetailed): void;
@@ -115,7 +116,7 @@ onMounted(() => {
os.api('users/show', {
userIds: defaultStore.state.recentlyUsedUsers,
}).then(users => {
- if (props.includeSelf) {
+ if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
recentUsers = [$i, ...users];
} else {
recentUsers = users;
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 516b88c13d..703c75c7d0 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -33,8 +33,8 @@
<button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
<div :class="$style.icon"><i class="ti ti-world-off"></i></div>
<div :class="$style.body">
- <span :class="$style.itemTitle">{{ i18n.ts._visibility.localOnly }}</span>
- <span :class="$style.itemDescription">{{ i18n.ts._visibility.localOnlyDescription }}</span>
+ <span :class="$style.itemTitle">{{ i18n.ts._visibility.disableFederation }}</span>
+ <span :class="$style.itemDescription">{{ i18n.ts._visibility.disableFederationDescription }}</span>
</div>
<div :class="$style.toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
</button>
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index e6dedd0354..84aae1cff8 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -24,7 +24,7 @@ const rawUrl = computed(() => {
return props.url;
}
if (props.host == null && !customEmojiName.value.includes('@')) {
- return customEmojis.value.find(x => x.name === customEmojiName.value)?.url || null;
+ return customEmojis.value.find(x => x.name === customEmojiName.value)?.url ?? null;
}
return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
});
@@ -32,7 +32,7 @@ const rawUrl = computed(() => {
const url = computed(() =>
defaultStore.reactiveState.disableShowingAnimatedImages.value && rawUrl.value
? getStaticImageUrl(rawUrl.value)
- : rawUrl.value
+ : rawUrl.value,
);
const alt = computed(() => `:${customEmojiName.value}:`);
@@ -41,7 +41,7 @@ let errored = $ref(url.value == null);
<style lang="scss" module>
.root {
- height: 2.5em;
+ height: 2em;
vertical-align: middle;
transition: transform 0.2s ease;
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index b181b62986..42760da08f 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -1,23 +1,33 @@
<template>
- <div ref="el" :class="$style.tabs" @wheel="onTabWheel">
- <div :class="$style.tabsInner">
- <button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
- class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
- @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)">
- <div :class="$style.tabInner">
- <i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
- <div v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)"
- :class="$style.tabTitle">{{ t.title }}</div>
- <Transition v-else mode="in-out" @enter="enter" @after-enter="afterEnter" @leave="leave"
- @after-leave="afterLeave">
- <div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
- </Transition>
+<div ref="el" :class="$style.tabs" @wheel="onTabWheel">
+ <div :class="$style.tabsInner">
+ <button
+ v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
+ class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
+ @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
+ >
+ <div :class="$style.tabInner">
+ <i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
+ <div
+ v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)"
+ :class="$style.tabTitle"
+ >
+ {{ t.title }}
</div>
- </button>
- </div>
- <div ref="tabHighlightEl"
- :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"></div>
+ <Transition
+ v-else mode="in-out" @enter="enter" @after-enter="afterEnter" @leave="leave"
+ @after-leave="afterLeave"
+ >
+ <div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
+ </Transition>
+ </div>
+ </button>
</div>
+ <div
+ ref="tabHighlightEl"
+ :class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"
+ ></div>
+</div>
</template>
<script lang="ts">
@@ -93,7 +103,7 @@ function onTabWheel(ev: WheelEvent) {
ev.stopPropagation();
(ev.currentTarget as HTMLElement).scrollBy({
left: ev.deltaY,
- behavior: 'smooth',
+ behavior: 'instant',
});
}
return false;
@@ -206,8 +216,8 @@ onUnmounted(() => {
align-items: center;
}
-.tabIcon+.tabTitle {
- padding-left: 8px;
+.tabIcon + .tabTitle {
+ padding-left: 4px;
}
.tabTitle {
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 98233b02e0..589ca92d75 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -2,9 +2,9 @@
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
- <MkAvatar :class="$style.avatar" :user="$i" />
+ <MkAvatar :class="$style.avatar" :user="$i"/>
</div>
- <div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft" />
+ <div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft"/>
<template v-if="metadata">
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
@@ -36,11 +36,11 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, inject } from 'vue';
import tinycolor from 'tinycolor2';
+import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll';
import { globalEvents } from '@/events';
import { injectPageMetadata } from '@/scripts/page-metadata';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
-import XTabs, { Tab } from './MkPageHeader.tabs.vue';
const props = withDefaults(defineProps<{
tabs?: Tab[];
@@ -96,7 +96,7 @@ function onTabClick(): void {
}
const calcBg = () => {
- const rawBg = metadata?.bg || 'var(--bg)';
+ const rawBg = metadata?.bg ?? 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
@@ -147,10 +147,7 @@ onUnmounted(() => {
.tabs:first-child {
margin-left: auto;
- }
- .tabs:not(:first-child) {
- padding-left: 16px;
- mask-image: linear-gradient(90deg, rgba(0,0,0,0), rgb(0,0,0) 16px, rgb(0,0,0) 100%);
+ padding: 0 12px;
}
.tabs {
margin-right: auto;
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 66c0bd5135..3fa8bb9adc 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -1,6 +1,7 @@
<template>
<time :title="absolute">
- <template v-if="mode === 'relative'">{{ relative }}</template>
+ <template v-if="invalid">{{ i18n.ts._ago.invalid }}</template>
+ <template v-else-if="mode === 'relative'">{{ relative }}</template>
<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
</time>
@@ -12,18 +13,24 @@ import { i18n } from '@/i18n';
import { dateTimeFormat } from '@/scripts/intl-const';
const props = withDefaults(defineProps<{
- time: Date | string;
+ time: Date | string | number | null;
mode?: 'relative' | 'absolute' | 'detail';
}>(), {
mode: 'relative',
});
-const _time = typeof props.time === 'string' ? new Date(props.time) : props.time;
-const absolute = dateTimeFormat.format(_time);
+const _time = props.time == null ? NaN :
+ typeof props.time === 'number' ? props.time :
+ (props.time instanceof Date ? props.time : new Date(props.time)).getTime();
+const invalid = Number.isNaN(_time);
+const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
-let now = $shallowRef(new Date());
-const relative = $computed(() => {
- const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+let now = $ref((new Date()).getTime());
+const relative = $computed<string>(() => {
+ if (props.mode === 'absolute') return ''; // absoluteではrelativeを使わないので計算しない
+ if (invalid) return i18n.ts._ago.invalid;
+
+ const ago = (now - _time) / 1000/*ms*/;
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() }) :
@@ -39,8 +46,8 @@ const relative = $computed(() => {
let tickId: number;
function tick() {
- now = new Date();
- const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+ now = (new Date()).getTime();
+ const ago = (now - _time) / 1000/*ms*/;
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
tickId = window.setTimeout(tick, next);
diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts
index 1b1d27ea2a..e84eabcbcc 100644
--- a/packages/frontend/src/components/mfm.ts
+++ b/packages/frontend/src/components/mfm.ts
@@ -278,7 +278,7 @@ export default defineComponent({
case 'hashtag': {
return [h(MkA, {
key: Math.random(),
- to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`,
+ to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
}, `#${token.props.hashtag}`)];
}