@@ -33,7 +33,7 @@ defineProps<{
reaction: string;
users: Misskey.entities.UserLite[];
count: number;
- targetElement: HTMLElement;
+ anchorElement: HTMLElement;
}>();
const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index e02d0ec21d..d96f0e2420 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -58,18 +58,22 @@ const emit = defineEmits<{
const buttonEl = useTemplateRef('buttonEl');
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
-const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction));
const canToggle = computed(() => {
+ const emoji = customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction);
+
// TODO
- //return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
- return !props.reaction.match(/@\w/) && $i && emoji.value;
+ //return !props.reaction.match(/@\w/) && $i && emoji && checkReactionPermissions($i, props.note, emoji);
+ return !props.reaction.match(/@\w/) && $i && emoji;
});
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
const isLocalCustomEmoji = props.reaction[0] === ':' && props.reaction.includes('@.');
async function toggleReaction() {
if (!canToggle.value) return;
+ if ($i == null) return;
+
+ const me = $i;
const oldReaction = props.myReaction;
if (oldReaction) {
@@ -93,7 +97,7 @@ async function toggleReaction() {
noteId: props.noteId,
}).then(() => {
noteEvents.emit(`unreacted:${props.noteId}`, {
- userId: $i!.id,
+ userId: me.id,
reaction: oldReaction,
});
if (oldReaction !== props.reaction) {
@@ -101,10 +105,12 @@ async function toggleReaction() {
noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
+ const emoji = customEmojisMap.get(emojiName.value);
+ if (emoji == null) return;
noteEvents.emit(`reacted:${props.noteId}`, {
- userId: $i!.id,
+ userId: me.id,
reaction: props.reaction,
- emoji: emoji.value,
+ emoji: emoji,
});
});
}
@@ -131,10 +137,13 @@ async function toggleReaction() {
noteId: props.noteId,
reaction: props.reaction,
}).then(() => {
+ const emoji = customEmojisMap.get(emojiName.value);
+ if (emoji == null) return;
+
noteEvents.emit(`reacted:${props.noteId}`, {
- userId: $i!.id,
+ userId: me.id,
reaction: props.reaction,
- emoji: emoji.value,
+ emoji: emoji,
});
});
// TODO: 上位コンポーネントでやる
@@ -217,6 +226,8 @@ onMounted(() => {
if (!mock) {
useTooltip(buttonEl, async (showing) => {
+ if (buttonEl.value == null) return;
+
const reactions = await misskeyApiGet('notes/reactions', {
noteId: props.noteId,
type: props.reaction,
@@ -231,7 +242,7 @@ if (!mock) {
reaction: props.reaction,
users,
count: props.count,
- targetElement: buttonEl.value,
+ anchorElement: buttonEl.value,
}, {
closed: () => dispose(),
});
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 15149b3f0c..8e5cbde8c3 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ role.name }}
-
+
{{ role.usersCount }} users
? users
@@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
- role: Misskey.entities.Role;
+ role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
forModeration: boolean;
detailed?: boolean;
}>(), {
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
index fc7ba50fb3..937804703d 100644
--- a/packages/frontend/src/components/MkRoleSelectDialog.vue
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -102,14 +102,12 @@ async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
- .map(r => ({ text: r.name, value: r }));
+ .map(r => ({ label: r.name, value: r.id }));
- const { canceled, result: role } = await os.select({ items });
- if (canceled) {
- return;
- }
+ const { canceled, result: roleId } = await os.select({ items });
+ if (canceled || roleId == null) return;
- selectedRoleIds.value.push(role.id);
+ selectedRoleIds.value.push(roleId);
}
async function removeRole(roleId: string) {
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 485d163ac4..e79236fe54 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -39,32 +39,42 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
diff --git a/packages/frontend/src/components/global/SearchKeyword.vue b/packages/frontend/src/components/global/SearchKeyword.vue
deleted file mode 100644
index 27a284faf0..0000000000
--- a/packages/frontend/src/components/global/SearchKeyword.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue
index 9e47517244..4c56767608 100644
--- a/packages/frontend/src/components/global/StackingRouterView.vue
+++ b/packages/frontend/src/components/global/StackingRouterView.vue
@@ -87,7 +87,7 @@ router.useListener('change', ({ resolved }) => {
const fullPath = router.getCurrentFullPath();
if (tabs.value.some(tab => tab.routePath === routePath && deepEqual(resolved.props, tab.props))) {
- const newTabs = [];
+ const newTabs = [] as typeof tabs.value;
for (const tab of tabs.value) {
newTabs.push(tab);
diff --git a/packages/frontend/src/components/grid/MkCellTooltip.vue b/packages/frontend/src/components/grid/MkCellTooltip.vue
index fd289c6cd9..6cd4f9ec1c 100644
--- a/packages/frontend/src/components/grid/MkCellTooltip.vue
+++ b/packages/frontend/src/components/grid/MkCellTooltip.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
+
{{ content }}
@@ -18,7 +18,7 @@ import MkTooltip from '@/components/MkTooltip.vue';
defineProps<{
showing: boolean;
content: string;
- targetElement: HTMLElement;
+ anchorElement: HTMLElement;
}>();
const emit = defineEmits<{
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index 444509e6b3..6f1dae8398 100644
--- a/packages/frontend/src/components/grid/MkDataCell.vue
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
![]()
{
const result = os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
showing,
content,
- targetElement: rootEl.value!,
+ anchorElement: rootEl.value!,
}, {
closed: () => {
result.dispose();
diff --git a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
index f85bf146e8..5ed8465299 100644
--- a/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
+++ b/packages/frontend/src/components/grid/MkGrid.stories.impl.ts
@@ -4,7 +4,7 @@
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { action } from '@storybook/addon-actions';
+import { action } from 'storybook/actions';
import type { StoryObj } from '@storybook/vue3';
import { ref } from 'vue';
import { commonHandlers } from '../../../.storybook/mocks.js';
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 6b1b80695f..eadf88ebd9 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -20,6 +20,7 @@ import NestedRouterView from './global/NestedRouterView.vue';
import StackingRouterView from './global/StackingRouterView.vue';
import MkLoading from './global/MkLoading.vue';
import MkError from './global/MkError.vue';
+import MkSuspense from './global/MkSuspense.vue';
import MkAd from './global/MkAd.vue';
import MkPageHeader from './global/MkPageHeader.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
@@ -60,6 +61,7 @@ export const components = {
MkUrl: MkUrl,
MkLoading: MkLoading,
MkError: MkError,
+ MkSuspense: MkSuspense,
MkAd: MkAd,
MkPageHeader: MkPageHeader,
MkStickyContainer: MkStickyContainer,
@@ -94,6 +96,7 @@ declare module '@vue/runtime-core' {
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
+ MkSuspense: typeof MkSuspense;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkStickyContainer: typeof MkStickyContainer;
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 69443ce7dd..7e8d8f7bfb 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js';
import MkMediaList from '@/components/MkMediaList.vue';
const props = defineProps<{
- block: Misskey.entities.PageBlock,
+ block: Extract
,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index df26874c17..bae05b9765 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -18,7 +18,7 @@ import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
const props = defineProps<{
- block: Misskey.entities.PageBlock,
+ block: Extract,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
index e3d26d924f..05c12b3b83 100644
--- a/packages/frontend/src/components/page/page.section.vue
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
defineProps<{
- block: Misskey.entities.PageBlock,
+ block: Extract,
h: number,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index a00eb0b5ca..792f6529d8 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -22,7 +22,7 @@ import { isEnabledUrlPreview } from '@/utility/url-preview.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
const props = defineProps<{
- block: Misskey.entities.PageBlock,
+ block: Extract,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/composables/use-lowres-time.ts b/packages/frontend/src/composables/use-lowres-time.ts
new file mode 100644
index 0000000000..3c5b561f51
--- /dev/null
+++ b/packages/frontend/src/composables/use-lowres-time.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, readonly, computed } from 'vue';
+
+const time = ref(Date.now());
+
+export const TIME_UPDATE_INTERVAL = 10000; // 10秒
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * ※ マウント前の時刻を返す可能性があるため、通常は`useLowresTime`を使用する
+*/
+export const lowresTime = readonly(time);
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * 必ず現在時刻以降を返すことを保証するコンポーサブル
+ */
+export function useLowresTime() {
+ // lowresTime自体はマウント前の時刻を返す可能性があるため、必ず現在時刻以降を返すことを保証する
+ const now = Date.now();
+ return computed(() => Math.max(time.value, now));
+}
+
+window.setInterval(() => {
+ time.value = Date.now();
+}, TIME_UPDATE_INTERVAL);
diff --git a/packages/frontend/src/composables/use-mkselect.ts b/packages/frontend/src/composables/use-mkselect.ts
new file mode 100644
index 0000000000..7cb470d169
--- /dev/null
+++ b/packages/frontend/src/composables/use-mkselect.ts
@@ -0,0 +1,38 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref } from 'vue';
+import type { Ref, MaybeRefOrGetter } from 'vue';
+import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
+
+type UnwrapReadonlyItems = T extends readonly (infer U)[] ? U[] : T;
+
+/** 指定したオプション定義をもとに型を狭めたrefを生成するコンポーサブル */
+export function useMkSelect<
+ const TItemsInput extends MaybeRefOrGetter,
+ const TItems extends TItemsInput extends MaybeRefOrGetter ? U : never,
+ TInitialValue extends OptionValue | void = void,
+ TItemsValue = GetMkSelectValueTypesFromDef>,
+ ModelType = TInitialValue extends void
+ ? TItemsValue
+ : (TItemsValue | TInitialValue)
+>(opts: {
+ items: TItemsInput;
+ initialValue?: (TInitialValue | (OptionValue extends TItemsValue ? OptionValue : TInitialValue)) & (
+ TItemsValue extends TInitialValue
+ ? unknown
+ : { 'Error: Type of initialValue must include all types of items': TItemsValue }
+ );
+}): {
+ def: TItemsInput;
+ model: Ref;
+} {
+ const model = ref(opts.initialValue ?? null);
+
+ return {
+ def: opts.items,
+ model: model as Ref,
+ };
+}
diff --git a/packages/frontend/src/composables/use-uploader.ts b/packages/frontend/src/composables/use-uploader.ts
index 826d8c5203..12b6e85940 100644
--- a/packages/frontend/src/composables/use-uploader.ts
+++ b/packages/frontend/src/composables/use-uploader.ts
@@ -43,6 +43,12 @@ const IMAGE_EDITING_SUPPORTED_TYPES = [
'image/webp',
];
+const VIDEO_COMPRESSION_SUPPORTED_TYPES = [ // TODO
+ 'video/mp4',
+ 'video/quicktime',
+ 'video/x-matroska',
+];
+
const WATERMARK_SUPPORTED_TYPES = IMAGE_EDITING_SUPPORTED_TYPES;
const IMAGE_PREPROCESS_NEEDED_TYPES = [
@@ -51,6 +57,10 @@ const IMAGE_PREPROCESS_NEEDED_TYPES = [
...IMAGE_EDITING_SUPPORTED_TYPES,
];
+const VIDEO_PREPROCESS_NEEDED_TYPES = [
+ ...VIDEO_COMPRESSION_SUPPORTED_TYPES,
+];
+
const mimeTypeMap = {
'image/webp': 'webp',
'image/jpeg': 'jpg',
@@ -64,6 +74,7 @@ export type UploaderItem = {
progress: { max: number; value: number } | null;
thumbnail: string | null;
preprocessing: boolean;
+ preprocessProgress: number | null;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
uploadFailed: boolean;
@@ -76,6 +87,7 @@ export type UploaderItem = {
isSensitive?: boolean;
caption?: string | null;
abort?: (() => void) | null;
+ abortPreprocess?: (() => void) | null;
};
function getCompressionSettings(level: 0 | 1 | 2 | 3) {
@@ -129,11 +141,12 @@ export function useUploader(options: {
progress: null,
thumbnail: THUMBNAIL_SUPPORTED_TYPES.includes(file.type) ? window.URL.createObjectURL(file) : null,
preprocessing: false,
+ preprocessProgress: null,
uploading: false,
aborted: false,
uploaded: null,
uploadFailed: false,
- compressionLevel: prefer.s.defaultImageCompressionLevel,
+ compressionLevel: IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultImageCompressionLevel : VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(file.type) ? prefer.s.defaultVideoCompressionLevel : 0,
watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file),
});
@@ -318,7 +331,7 @@ export function useUploader(options: {
}
if (
- IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) &&
+ (IMAGE_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type) || VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(item.file.type)) &&
!item.preprocessing &&
!item.uploading &&
!item.uploaded
@@ -391,6 +404,19 @@ export function useUploader(options: {
removeItem(item);
},
});
+ } else if (item.preprocessing && item.abortPreprocess != null) {
+ menu.push({
+ type: 'divider',
+ }, {
+ icon: 'ti ti-player-stop',
+ text: i18n.ts.abort,
+ danger: true,
+ action: () => {
+ if (item.abortPreprocess != null) {
+ item.abortPreprocess();
+ }
+ },
+ });
} else if (item.uploading) {
menu.push({
type: 'divider',
@@ -474,6 +500,10 @@ export function useUploader(options: {
continue;
}
+ if (item.abortPreprocess != null) {
+ item.abortPreprocess();
+ }
+
if (item.abort != null) {
item.abort();
}
@@ -484,18 +514,30 @@ export function useUploader(options: {
async function preprocess(item: UploaderItem): Promise {
item.preprocessing = true;
+ item.preprocessProgress = null;
- try {
- if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
+ if (IMAGE_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
+ try {
await preprocessForImage(item);
- }
- } catch (err) {
- console.error('Failed to preprocess image', err);
+ } catch (err) {
+ console.error('Failed to preprocess image', err);
// nop
+ }
+ }
+
+ if (VIDEO_PREPROCESS_NEEDED_TYPES.includes(item.file.type)) {
+ try {
+ await preprocessForVideo(item);
+ } catch (err) {
+ console.error('Failed to preprocess video', err);
+
+ // nop
+ }
}
item.preprocessing = false;
+ item.preprocessProgress = null;
}
async function preprocessForImage(item: UploaderItem): Promise {
@@ -564,10 +606,74 @@ export function useUploader(options: {
item.preprocessedFile = markRaw(preprocessedFile);
}
- onUnmounted(() => {
+ async function preprocessForVideo(item: UploaderItem): Promise {
+ let preprocessedFile: Blob | File = item.file;
+
+ const needsCompress = item.compressionLevel !== 0 && VIDEO_COMPRESSION_SUPPORTED_TYPES.includes(preprocessedFile.type);
+
+ if (needsCompress) {
+ const mediabunny = await import('mediabunny');
+
+ const source = new mediabunny.BlobSource(preprocessedFile);
+
+ const input = new mediabunny.Input({
+ source,
+ formats: mediabunny.ALL_FORMATS,
+ });
+
+ const output = new mediabunny.Output({
+ target: new mediabunny.BufferTarget(),
+ format: new mediabunny.Mp4OutputFormat(),
+ });
+
+ const currentConversion = await mediabunny.Conversion.init({
+ input,
+ output,
+ video: {
+ //width: 320, // Height will be deduced automatically to retain aspect ratio
+ bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
+ },
+ audio: {
+ bitrate: item.compressionLevel === 1 ? mediabunny.QUALITY_VERY_HIGH : item.compressionLevel === 2 ? mediabunny.QUALITY_MEDIUM : mediabunny.QUALITY_VERY_LOW,
+ },
+ });
+
+ currentConversion.onProgress = newProgress => item.preprocessProgress = newProgress;
+
+ item.abortPreprocess = () => {
+ item.abortPreprocess = null;
+ currentConversion.cancel();
+ item.preprocessing = false;
+ item.preprocessProgress = null;
+ };
+
+ await currentConversion.execute();
+
+ item.abortPreprocess = null;
+
+ preprocessedFile = new Blob([output.target.buffer!], { type: output.format.mimeType });
+ item.compressedSize = output.target.buffer!.byteLength;
+ item.uploadName = `${item.name}.mp4`;
+ } else {
+ item.compressedSize = null;
+ item.uploadName = item.name;
+ }
+
+ if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
+ item.thumbnail = THUMBNAIL_SUPPORTED_TYPES.includes(preprocessedFile.type) ? window.URL.createObjectURL(preprocessedFile) : null;
+ item.preprocessedFile = markRaw(preprocessedFile);
+ }
+
+ function dispose() {
for (const item of items.value) {
if (item.thumbnail != null) URL.revokeObjectURL(item.thumbnail);
}
+
+ abortAll();
+ }
+
+ onUnmounted(() => {
+ dispose();
});
return {
@@ -575,6 +681,7 @@ export function useUploader(options: {
addFiles,
removeItem,
abortAll,
+ dispose,
upload,
getMenu,
uploading: computed(() => items.value.some(item => item.uploading)),
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 750acd0588..62aecbc87c 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -57,7 +57,7 @@ export default {
text: self.text,
asMfm: binding.modifiers.mfm,
direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top',
- targetElement: el,
+ anchorElement: el,
}, {
closed: () => dispose(),
});
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
index 94deea82c7..b11ef8f088 100644
--- a/packages/frontend/src/directives/user-preview.ts
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -6,6 +6,7 @@
import { defineAsyncComponent, ref } from 'vue';
import type { Directive } from 'vue';
import { popup } from '@/os.js';
+import { isTouchUsing } from '@/utility/touch.js';
export class UserPreview {
private el;
@@ -107,6 +108,7 @@ export class UserPreview {
export default {
mounted(el: HTMLElement, binding, vn) {
if (binding.value == null) return;
+ if (isTouchUsing) return;
// TODO: 新たにプロパティを作るのをやめMapを使う
// ただメモリ的には↓の方が省メモリかもしれないので検討中
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index 649561cd75..8cac1b6d2a 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -24,7 +24,7 @@ export const globalEvents = new EventEmitter();
export function useGlobalEvent(
event: T,
- callback: Events[T],
+ callback: EventEmitter.EventListener,
): void {
globalEvents.on(event, callback);
onBeforeUnmount(() => {
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index a5397f0c0d..c9d83a4dbe 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -51,3 +51,9 @@ export async function fetchInstance(force = false): Promise {
private mergeState(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
- const merged = deepMerge(value, def);
+ const merged = deepMerge>(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index c0fe0f2b85..a162b3aa9e 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -66,6 +66,12 @@ export const navbarItemDef = reactive({
lookup();
},
},
+ qr: {
+ title: i18n.ts.qr,
+ icon: 'ti ti-qrcode',
+ show: computed(() => $i != null),
+ to: '/qr',
+ },
lists: {
title: i18n.ts.lists,
icon: 'ti ti-list',
@@ -111,7 +117,7 @@ export const navbarItemDef = reactive({
to: '/channels',
},
chat: {
- title: i18n.ts.chat,
+ title: i18n.ts.directMessage_short,
icon: 'ti ti-messages',
to: '/chat',
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index bf0e5e1b37..6c5f04c6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -9,11 +9,12 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { Component, Ref } from 'vue';
-import type { ComponentProps as CP } from 'vue-component-type-helpers';
+import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
+import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -35,9 +36,9 @@ import { focusParent } from '@/utility/focus.js';
export const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record;
-export const apiWithDialog = ((
+export const apiWithDialog = ((
endpoint: E,
- data: P,
+ data: Misskey.Endpoints[E]['req'],
token?: string | null | undefined,
customErrors?: ApiWithDialogCustomErrors,
) => {
@@ -157,28 +158,9 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
return zIndexes[priority];
}
-// InstanceType['$emit'] だとインターセクション型が返ってきて
-// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
-// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
-type ComponentEmit = T extends new () => { $props: infer Props }
- ? [keyof Pick>] extends [never]
- ? Record // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
- : EmitsExtractor
- : T extends (...args: any) => any
- ? ReturnType extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
- ? [keyof Pick>] extends [never]
- ? Record
- : EmitsExtractor
- : never
- : never;
-
// props に ref を許可するようにする
type ComponentProps = { [K in keyof CP]: CP[K] | Ref[K]> };
-type EmitsExtractor = {
- [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize : K extends string ? never : K]: T[K];
-};
-
export function popup(
component: T,
props: ComponentProps,
@@ -521,50 +503,15 @@ export function authenticateDialog(): Promise<{
});
}
-type SelectItem = {
- value: C;
- text: string;
-};
-
-// default が指定されていたら result は null になり得ないことを保証する overload function
-export function select(props: {
+export function select(props: {
title?: string;
text?: string;
- default: string;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
+ default?: D;
+ items: (MkSelectItem | undefined)[];
}): Promise<{
canceled: true; result: undefined;
} | {
- canceled: false; result: C;
-}>;
-export function select(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C | null;
-}>;
-export function select(props: {
- title?: string;
- text?: string;
- default?: string | null;
- items: (SelectItem | {
- sectionTitle: string;
- items: SelectItem[];
- } | undefined)[];
-}): Promise<{
- canceled: true; result: undefined;
-} | {
- canceled: false; result: C | null;
+ canceled: false; result: Exclude extends null ? C | null : C;
}> {
return new Promise(resolve => {
const { dispose } = popup(MkDialog, {
@@ -703,7 +650,7 @@ export async function cropImageFile(imageFile: File | Blob, options: {
});
}
-export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | EventTarget | null, options?: {
+export function popupMenu(items: (MenuItem | null)[], anchorElement?: HTMLElement | EventTarget | null, options?: {
align?: string;
width?: number;
onClosing?: () => void;
@@ -715,7 +662,7 @@ export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | Event
let returnFocusTo = getHTMLElementOrNull(anchorElement) ?? getHTMLElementOrNull(window.document.activeElement);
return new Promise(resolve => nextTick(() => {
const { dispose } = popup(MkPopupMenu, {
- items,
+ items: items.filter(x => x != null),
anchorElement,
width: options?.width,
align: options?.align,
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index b166dfd940..4640812756 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -11,12 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-