mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-14 13:25:48 +02:00
Merge branch 'develop' into room
This commit is contained in:
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, i) in modelValue"
|
||||
:key="item.id"
|
||||
:key="`MkDraggableRoot:${item.id}`"
|
||||
:class="$style.item"
|
||||
:draggable="!manualDragStart"
|
||||
@dragstart.stop="onDragstart($event, item)"
|
||||
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@dragleave="onDragleave($event, item)"
|
||||
@drop.prevent.stop="onDrop($event, item, false)"
|
||||
></div>
|
||||
<div style="position: relative; z-index: 0;">
|
||||
<div :key="`MkDraggableItem:${item.id}`" style="position: relative; z-index: 0;">
|
||||
<slot :item="item" :index="i" :dragStart="(ev) => onDragstart(ev, item)"></slot>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -64,7 +64,7 @@ defineProps<{
|
||||
const params = defineModel<Record<string, any>>({ required: true });
|
||||
|
||||
function getHex(c: ImageEffectorRGB) {
|
||||
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
|
||||
return `#${c.map(x => Math.round(x * 255).toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
function getRgb(hex: string | number): ImageEffectorRGB | null {
|
||||
|
||||
@@ -156,11 +156,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
|
||||
import ExifReader from 'exifreader';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import MkPreviewWithControls from './MkPreviewWithControls.vue';
|
||||
import type { ImageFrameParams, ImageFramePreset } from '@/utility/image-frame-renderer/ImageFrameRenderer.js';
|
||||
import { ImageFrameRenderer } from '@/utility/image-frame-renderer/ImageFrameRenderer.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkPreviewWithControls from './MkPreviewWithControls.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
@@ -390,7 +390,7 @@ async function save() {
|
||||
}
|
||||
|
||||
function getHex(c: [number, number, number]) {
|
||||
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
|
||||
return `#${c.map(x => Math.round(x * 255).toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
function getRgb(hex: string | number): [number, number, number] | null {
|
||||
|
||||
@@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
|
||||
<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<span>{{ notification.message }}</span>
|
||||
<Mfm :text="notification.message" :author="notification.user" :plain="true" :nowrap="true"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
|
||||
import { watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted, onBeforeUnmount } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
@@ -227,6 +227,10 @@ const targetChannel = shallowRef(props.channel);
|
||||
const serverDraftId = ref<string | null>(null);
|
||||
const postFormActions = getPluginHandlers('post_form_action');
|
||||
|
||||
let textAutocomplete: Autocomplete | null = null;
|
||||
let cwAutocomplete: Autocomplete | null = null;
|
||||
let hashtagAutocomplete: Autocomplete | null = null;
|
||||
|
||||
const uploader = useUploader({
|
||||
multiple: true,
|
||||
});
|
||||
@@ -1408,10 +1412,9 @@ onMounted(() => {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: detach when unmount
|
||||
if (textareaEl.value) new Autocomplete(textareaEl.value, text);
|
||||
if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw);
|
||||
if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags);
|
||||
if (textareaEl.value) textAutocomplete = new Autocomplete(textareaEl.value, text);
|
||||
if (cwInputEl.value) cwAutocomplete = new Autocomplete(cwInputEl.value, cw);
|
||||
if (hashtagsInputEl.value) hashtagAutocomplete = new Autocomplete(hashtagsInputEl.value, hashtags);
|
||||
|
||||
nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
@@ -1468,6 +1471,19 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uploader.abortAll();
|
||||
if (textAutocomplete) {
|
||||
textAutocomplete.detach();
|
||||
}
|
||||
if (cwAutocomplete) {
|
||||
cwAutocomplete.detach();
|
||||
}
|
||||
if (hashtagAutocomplete) {
|
||||
hashtagAutocomplete.detach();
|
||||
}
|
||||
});
|
||||
|
||||
async function canClose() {
|
||||
if (!uploader.allItemsUploaded.value) {
|
||||
const { canceled } = await os.confirm({
|
||||
|
||||
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
</template>
|
||||
</span>
|
||||
<span :class="$style.headerTitle" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
|
||||
<span :class="$style.headerTitle" @pointerdown.prevent="onHeaderPointerdown">
|
||||
<slot name="header"></slot>
|
||||
</span>
|
||||
<span :class="$style.headerRight">
|
||||
@@ -39,14 +39,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="canResize && !minimized">
|
||||
<div :class="$style.handleTop" @mousedown.prevent="onTopHandleMousedown"></div>
|
||||
<div :class="$style.handleRight" @mousedown.prevent="onRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottom" @mousedown.prevent="onBottomHandleMousedown"></div>
|
||||
<div :class="$style.handleLeft" @mousedown.prevent="onLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTopLeft" @mousedown.prevent="onTopLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTopRight" @mousedown.prevent="onTopRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottomRight" @mousedown.prevent="onBottomRightHandleMousedown"></div>
|
||||
<div :class="$style.handleBottomLeft" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
|
||||
<div :class="$style.handleTop" @pointerdown.prevent="onTopHandlePointerdown"></div>
|
||||
<div :class="$style.handleRight" @pointerdown.prevent="onRightHandlePointerdown"></div>
|
||||
<div :class="$style.handleBottom" @pointerdown.prevent="onBottomHandlePointerdown"></div>
|
||||
<div :class="$style.handleLeft" @pointerdown.prevent="onLeftHandlePointerdown"></div>
|
||||
<div :class="$style.handleTopLeft" @pointerdown.prevent="onTopLeftHandlePointerdown"></div>
|
||||
<div :class="$style.handleTopRight" @pointerdown.prevent="onTopRightHandlePointerdown"></div>
|
||||
<div :class="$style.handleBottomRight" @pointerdown.prevent="onBottomRightHandlePointerdown"></div>
|
||||
<div :class="$style.handleBottomLeft" @pointerdown.prevent="onBottomLeftHandlePointerdown"></div>
|
||||
</template>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -70,20 +70,39 @@ type WindowButton = {
|
||||
const minHeight = 50;
|
||||
const minWidth = 250;
|
||||
|
||||
function dragListen(fn: (ev: MouseEvent | TouchEvent) => void) {
|
||||
window.addEventListener('mousemove', fn);
|
||||
window.addEventListener('touchmove', fn);
|
||||
window.addEventListener('mouseleave', dragClear.bind(null, fn));
|
||||
window.addEventListener('mouseup', dragClear.bind(null, fn));
|
||||
window.addEventListener('touchend', dragClear.bind(null, fn));
|
||||
function dragListen(fn: (ev: PointerEvent) => void) {
|
||||
window.addEventListener('pointermove', fn);
|
||||
const clear = () => {
|
||||
dragClear(fn);
|
||||
};
|
||||
window.addEventListener('pointerup', clear, { once: true });
|
||||
window.addEventListener('pointercancel', clear, { once: true });
|
||||
window.addEventListener('blur', clear, { once: true });
|
||||
}
|
||||
|
||||
function dragClear(fn: (ev: MouseEvent | TouchEvent) => void) {
|
||||
window.removeEventListener('mousemove', fn);
|
||||
window.removeEventListener('touchmove', fn);
|
||||
window.removeEventListener('mouseleave', dragClear as any);
|
||||
window.removeEventListener('mouseup', dragClear as any);
|
||||
window.removeEventListener('touchend', dragClear as any);
|
||||
function dragClear(fn: (ev: PointerEvent) => void) {
|
||||
window.removeEventListener('pointermove', fn);
|
||||
}
|
||||
|
||||
function capturePointer(evt: PointerEvent) {
|
||||
const target = evt.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
if (!target.setPointerCapture) return;
|
||||
|
||||
try {
|
||||
target.setPointerCapture(evt.pointerId);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const release = () => {
|
||||
if (target.hasPointerCapture(evt.pointerId)) {
|
||||
target.releasePointerCapture(evt.pointerId);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pointerup', release, { once: true });
|
||||
window.addEventListener('pointercancel', release, { once: true });
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -209,15 +228,17 @@ function onDblClick() {
|
||||
}
|
||||
}
|
||||
|
||||
function getPositionX(event: MouseEvent | TouchEvent) {
|
||||
return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientX : 'clientX' in event ? event.clientX : 0;
|
||||
function getPositionX(event: PointerEvent) {
|
||||
return event.clientX;
|
||||
}
|
||||
|
||||
function getPositionY(event: MouseEvent | TouchEvent) {
|
||||
return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientY : 'clientY' in event ? event.clientY : 0;
|
||||
function getPositionY(event: PointerEvent) {
|
||||
return event.clientY;
|
||||
}
|
||||
|
||||
function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
|
||||
function onHeaderPointerdown(evt: PointerEvent) {
|
||||
capturePointer(evt);
|
||||
|
||||
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
|
||||
if ('button' in evt && evt.button === 2) return;
|
||||
|
||||
@@ -289,7 +310,9 @@ function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
|
||||
}
|
||||
|
||||
// 上ハンドル掴み時
|
||||
function onTopHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
function onTopHandlePointerdown(evt: PointerEvent) {
|
||||
capturePointer(evt);
|
||||
|
||||
const main = rootEl.value;
|
||||
// どういうわけかnullになることがある
|
||||
if (main == null) return;
|
||||
@@ -317,7 +340,9 @@ function onTopHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
}
|
||||
|
||||
// 右ハンドル掴み時
|
||||
function onRightHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
function onRightHandlePointerdown(evt: PointerEvent) {
|
||||
capturePointer(evt);
|
||||
|
||||
const main = rootEl.value;
|
||||
if (main == null) return;
|
||||
|
||||
@@ -342,7 +367,9 @@ function onRightHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
}
|
||||
|
||||
// 下ハンドル掴み時
|
||||
function onBottomHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
function onBottomHandlePointerdown(evt: PointerEvent) {
|
||||
capturePointer(evt);
|
||||
|
||||
const main = rootEl.value;
|
||||
if (main == null) return;
|
||||
|
||||
@@ -367,7 +394,9 @@ function onBottomHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
}
|
||||
|
||||
// 左ハンドル掴み時
|
||||
function onLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
function onLeftHandlePointerdown(evt: PointerEvent) {
|
||||
capturePointer(evt);
|
||||
|
||||
const main = rootEl.value;
|
||||
if (main == null) return;
|
||||
|
||||
@@ -394,27 +423,27 @@ function onLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
}
|
||||
|
||||
// 左上ハンドル掴み時
|
||||
function onTopLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
onTopHandleMousedown(evt);
|
||||
onLeftHandleMousedown(evt);
|
||||
function onTopLeftHandlePointerdown(evt: PointerEvent) {
|
||||
onTopHandlePointerdown(evt);
|
||||
onLeftHandlePointerdown(evt);
|
||||
}
|
||||
|
||||
// 右上ハンドル掴み時
|
||||
function onTopRightHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
onTopHandleMousedown(evt);
|
||||
onRightHandleMousedown(evt);
|
||||
function onTopRightHandlePointerdown(evt: PointerEvent) {
|
||||
onTopHandlePointerdown(evt);
|
||||
onRightHandlePointerdown(evt);
|
||||
}
|
||||
|
||||
// 右下ハンドル掴み時
|
||||
function onBottomRightHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
onBottomHandleMousedown(evt);
|
||||
onRightHandleMousedown(evt);
|
||||
function onBottomRightHandlePointerdown(evt: PointerEvent) {
|
||||
onBottomHandlePointerdown(evt);
|
||||
onRightHandlePointerdown(evt);
|
||||
}
|
||||
|
||||
// 左下ハンドル掴み時
|
||||
function onBottomLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
|
||||
onBottomHandleMousedown(evt);
|
||||
onLeftHandleMousedown(evt);
|
||||
function onBottomLeftHandlePointerdown(evt: PointerEvent) {
|
||||
onBottomHandlePointerdown(evt);
|
||||
onLeftHandlePointerdown(evt);
|
||||
}
|
||||
|
||||
// 高さを適用
|
||||
@@ -566,6 +595,7 @@ defineExpose({
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -579,6 +609,7 @@ $handleSize: 8px;
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.handleTop {
|
||||
|
||||
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
|
||||
<div class="_gaps_s">
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="!storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
|
||||
<MkInfo v-if="storagePersistenceSupported && !storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
|
||||
<div>{{ i18n.ts._settings.settingsPersistence_description1 }}</div>
|
||||
<div>{{ i18n.ts._settings.settingsPersistence_description2 }}</div>
|
||||
<div><button class="_textButton" @click="enableStoragePersistence">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipStoragePersistence">{{ i18n.ts.skip }}</button></div>
|
||||
@@ -51,7 +51,7 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
|
||||
import { store } from '@/store.js';
|
||||
import { signout } from '@/signout.js';
|
||||
import { genSearchIndexes } from '@/utility/inapp-search.js';
|
||||
import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
|
||||
import { enableStoragePersistence, storagePersisted, storagePersistenceSupported, skipStoragePersistence } from '@/utility/storage.js';
|
||||
|
||||
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
<MkButton v-if="!storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
|
||||
<MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
|
||||
|
||||
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
|
||||
|
||||
@@ -165,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
|
||||
import { enableStoragePersistence, storagePersisted, storagePersistenceSupported } from '@/utility/storage.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
|
||||
@@ -25,6 +25,7 @@ const props = defineProps<{
|
||||
|
||||
const paginator = markRaw(new Paginator('hashtags/users', {
|
||||
limit: 30,
|
||||
offsetMode: true,
|
||||
computedParams: computed(() => ({
|
||||
tag: props.tag,
|
||||
origin: 'combined',
|
||||
|
||||
@@ -18,7 +18,6 @@ let lastHeartbeatCall = 0;
|
||||
export function useStream(): Misskey.IStream {
|
||||
if (stream) return stream;
|
||||
|
||||
// TODO: No Websocketモードもここで判定
|
||||
stream = markRaw(new Misskey.Stream(wsOrigin, $i ? {
|
||||
token: $i.token,
|
||||
} : null));
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { store } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
export const storagePersisted = ref(await navigator.storage.persisted());
|
||||
export const storagePersistenceSupported = window.isSecureContext && 'storage' in navigator;
|
||||
export const storagePersisted = ref(storagePersistenceSupported ? await navigator.storage.persisted() : false);
|
||||
|
||||
export async function enableStoragePersistence() {
|
||||
if (!storagePersistenceSupported) return;
|
||||
try {
|
||||
const persisted = await navigator.storage.persist();
|
||||
if (persisted) {
|
||||
|
||||
Reference in New Issue
Block a user