Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina
2021-12-11 00:57:22 +09:00
635 changed files with 4867 additions and 4789 deletions

View File

@@ -11,10 +11,8 @@
},
"dependencies": {
"@discordapp/twemoji": "13.1.0",
"@elastic/elasticsearch": "7.11.0",
"@sentry/browser": "5.29.2",
"@sentry/tracing": "5.29.2",
"@sinonjs/fake-timers": "7.1.2",
"@syuilo/aiscript": "0.11.1",
"@types/dateformat": "3.0.1",
"@types/escape-regexp": "0.0.0",
@@ -22,70 +20,56 @@
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"@types/is-url": "1.2.30",
"@types/js-yaml": "4.0.4",
"@types/katex": "0.11.1",
"@types/matter-js": "0.17.6",
"@types/mocha": "8.2.3",
"@types/node": "16.11.7",
"@types/node-fetch": "2.5.12",
"@types/nodemailer": "6.4.4",
"@types/nprogress": "0.2.0",
"@types/node": "16.11.12",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.2",
"@types/parsimmon": "1.10.6",
"@types/portscanner": "2.1.1",
"@types/pug": "2.0.5",
"@types/parse5": "6.0.3",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.1",
"@types/random-seed": "0.3.3",
"@types/rename": "1.0.4",
"@types/request-stats": "3.0.0",
"@types/seedrandom": "2.4.28",
"@types/sinonjs__fake-timers": "6.0.4",
"@types/speakeasy": "2.0.6",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.2",
"@types/uuid": "8.3.1",
"@types/uuid": "8.3.3",
"@types/web-push": "3.3.2",
"@types/webpack": "5.28.0",
"@types/webpack-stream": "3.2.12",
"@types/websocket": "1.0.4",
"@types/ws": "8.2.0",
"@typescript-eslint/parser": "5.1.0",
"@vue/compiler-sfc": "3.2.21",
"@types/ws": "8.2.2",
"@typescript-eslint/parser": "5.6.0",
"@vue/compiler-sfc": "3.2.24",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "4.0.4",
"autwh": "0.1.0",
"blurhash": "1.1.4",
"broadcast-channel": "4.5.0",
"chart.js": "3.6.0",
"broadcast-channel": "4.7.0",
"chart.js": "3.6.2",
"chartjs-adapter-date-fns": "2.0.0",
"chartjs-plugin-zoom": "1.1.1",
"chartjs-plugin-zoom": "1.2.0",
"compare-versions": "3.6.0",
"concurrently": "6.3.0",
"content-disposition": "0.5.3",
"crc-32": "1.2.0",
"css-loader": "6.5.1",
"cssnano": "5.0.10",
"date-fns": "2.25.0",
"cssnano": "5.0.12",
"date-fns": "2.27.0",
"dateformat": "4.5.1",
"escape-regexp": "0.0.1",
"eslint": "8.2.0",
"eslint-plugin-vue": "8.1.1",
"eslint": "8.4.1",
"eslint-plugin-vue": "8.2.0",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
"glob": "7.2.0",
"got": "11.8.2",
"idb-keyval": "5.1.3",
"insert-text-at-cursor": "0.3.0",
"ip-cidr": "3.0.4",
"is-svg": "4.3.1",
"js-yaml": "4.1.0",
"json5": "2.2.0",
"json5-loader": "4.0.1",
"katex": "0.13.18",
"katex": "0.15.1",
"langmap": "0.0.16",
"matter-js": "0.17.1",
"mfm-js": "0.20.0",
@@ -93,32 +77,26 @@
"mocha": "8.4.0",
"ms": "2.1.3",
"nested-property": "4.0.0",
"node-fetch": "2.6.1",
"parse5": "6.0.1",
"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
"portscanner": "2.2.0",
"postcss": "8.3.11",
"postcss-loader": "6.2.0",
"postcss": "8.4.4",
"postcss-loader": "6.2.1",
"prismjs": "1.25.0",
"private-ip": "2.3.3",
"probe-image-size": "7.2.1",
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.1.1",
"pureimage": "0.3.5",
"qrcode": "1.4.4",
"qrcode": "1.5.0",
"querystring": "0.2.1",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
"request-stats": "3.0.0",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.43.4",
"sass-loader": "12.3.0",
"sass": "1.44.0",
"sass-loader": "12.4.0",
"seedrandom": "3.0.5",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
@@ -130,25 +108,25 @@
"tmp": "0.2.1",
"ts-loader": "9.2.6",
"ts-node": "10.4.0",
"tsc-alias": "1.3.10",
"tsconfig-paths": "3.11.0",
"tsc-alias": "1.4.2",
"tsconfig-paths": "3.12.0",
"twemoji-parser": "13.1.0",
"typescript": "4.4.4",
"typescript": "4.5.2",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vue": "3.2.21",
"vue-loader": "16.7.0",
"vue": "3.2.24",
"vue-loader": "16.8.3",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.5",
"vue-style-loader": "4.1.3",
"vue-svg-loader": "0.17.0-beta.2",
"vuedraggable": "4.0.1",
"web-push": "3.4.5",
"webpack": "5.63.0",
"webpack": "5.65.0",
"webpack-cli": "4.9.1",
"websocket": "1.0.34",
"ws": "8.2.3"
"ws": "8.3.0"
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.54",

View File

@@ -85,6 +85,10 @@ export default defineComponent({
<style lang="scss" scoped>
.zdjebgpv {
position: relative;
display: flex;
background: #e1e1e1;
border-radius: 8px;
overflow: clip;
> .icon-sub {
position: absolute;
@@ -95,14 +99,11 @@ export default defineComponent({
bottom: 4%;
}
> * {
margin: auto;
}
> .icon {
pointer-events: none;
height: 65%;
width: 65%;
margin: auto;
font-size: 32px;
color: #777;
}
}
</style>

View File

@@ -210,7 +210,7 @@ export default defineComponent({
position: relative;
padding: 8px 0 0 0;
min-height: 180px;
border-radius: 4px;
border-radius: 8px;
&, * {
cursor: pointer;

View File

@@ -657,14 +657,14 @@ export default defineComponent({
> .path {
display: inline-block;
vertical-align: bottom;
line-height: 38px;
line-height: 50px;
white-space: nowrap;
> * {
display: inline-block;
margin: 0;
padding: 0 8px;
line-height: 38px;
line-height: 50px;
cursor: pointer;
* {
@@ -699,6 +699,7 @@ export default defineComponent({
> .menu {
margin-left: auto;
padding: 0 12px;
}
}

View File

@@ -79,7 +79,7 @@ import { emojilist } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Particle from '@/components/particle.vue';
import * as os from '@/os';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import { isTouchUsing } from '@/scripts/touch';
import { isMobile } from '@/scripts/is-mobile';
import { emojiCategories } from '@/instance';
import XSection from './emoji-picker.section.vue';
@@ -108,7 +108,7 @@ export default defineComponent({
pinned: this.$store.reactiveState.reactions,
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
big: this.asReactionPicker ? isDeviceTouch : false,
big: this.asReactionPicker ? isTouchUsing : false,
customEmojiCategories: emojiCategories,
customEmojis: this.$instance.emojis,
q: null,
@@ -268,7 +268,7 @@ export default defineComponent({
methods: {
focus() {
if (!isMobile && !isDeviceTouch) {
if (!isMobile && !isTouchUsing) {
this.$refs.search.focus({
preventScroll: true
});

View File

@@ -6,9 +6,11 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent, ref, watch } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { defaultStore } from '@/store';
import { instance } from '@/instance';
export default defineComponent({
props: {
@@ -35,61 +37,33 @@ export default defineComponent({
},
},
data() {
setup(props) {
const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? null : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
const ce = computed(() => props.customEmojis || instance.emojis || []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
const url = computed(() => {
if (char.value) {
let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
codes = codes.filter(x => x && x.length);
return `${twemojiSvgBase}/${codes.join('-')}.svg`;
} else {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.value.url)
: customEmoji.value.url;
}
});
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
return {
url: null,
char: null,
customEmoji: null
}
},
computed: {
isCustom(): boolean {
return this.emoji.startsWith(':');
},
alt(): string {
return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
},
useOsNativeEmojis(): boolean {
return this.$store.state.useOsNativeEmojis && !this.isReaction;
},
ce() {
return this.customEmojis || this.$instance?.emojis || [];
}
},
watch: {
ce: {
handler() {
if (this.isCustom) {
const customEmoji = this.ce.find(x => x.name === this.emoji.substr(1, this.emoji.length - 2));
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = this.$store.state.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.url)
: customEmoji.url;
}
}
},
immediate: true
},
},
created() {
if (!this.isCustom) {
this.char = this.emoji;
}
if (this.char) {
let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
codes = codes.filter(x => x && x.length);
this.url = `${twemojiSvgBase}/${codes.join('-')}.svg`;
}
url,
char,
alt,
customEmoji,
useOsNativeEmojis,
};
},
});
</script>

View File

@@ -23,7 +23,7 @@
import { defineComponent } from 'vue';
import { toUnicode as decodePunycode } from 'punycode/';
import { url as local } from '@/config';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import { isTouchUsing } from '@/scripts/touch';
import * as os from '@/os';
export default defineComponent({
@@ -91,13 +91,13 @@ export default defineComponent({
}
},
onMouseover() {
if (isDeviceTouch) return;
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
if (isDeviceTouch) return;
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);

View File

@@ -12,7 +12,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { url as local } from '@/config';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import { isTouchUsing } from '@/scripts/touch';
import * as os from '@/os';
export default defineComponent({
@@ -65,13 +65,13 @@ export default defineComponent({
}
},
onMouseover() {
if (isDeviceTouch) return;
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
if (isDeviceTouch) return;
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);

View File

@@ -8,7 +8,7 @@
</div>
</div>
</div>
<div v-else class="gqnyydlz" :style="{ background: color }">
<div v-else class="gqnyydlz">
<a
:href="image.url"
:title="image.name"
@@ -16,15 +16,13 @@
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
</a>
<i class="fas fa-eye-slash" @click="hide = true"></i>
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="fas fa-eye-slash"></i></button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import ImageViewer from './image-viewer.vue';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import * as os from '@/os';
@@ -44,7 +42,6 @@ export default defineComponent({
data() {
return {
hide: true,
color: null,
};
},
computed: {
@@ -64,9 +61,6 @@ export default defineComponent({
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
this.$watch('image', () => {
this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
}
}, {
deep: true,
immediate: true,
@@ -109,21 +103,26 @@ export default defineComponent({
.gqnyydlz {
position: relative;
border: solid 0.5px var(--divider);
//box-shadow: 0 0 0 1px var(--divider) inset;
background: var(--bg);
> i {
> .hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
font-size: 14px;
opacity: .5;
padding: 3px 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
cursor: pointer;
top: 12px;
right: 12px;
> i {
display: block;
}
}
> a {

View File

@@ -130,7 +130,7 @@ export default defineComponent({
bottom: 0;
left: 0;
display: grid;
grid-gap: 4px;
grid-gap: 8px;
> * {
overflow: hidden;

View File

@@ -19,10 +19,6 @@
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis"
:no-style="true"
@touchstart.passive="onReactionMouseover"
@mouseover="onReactionMouseover"
@mouseleave="onReactionMouseleave"
@touchend="onReactionMouseleave"
/>
</div>
</div>
@@ -155,7 +151,7 @@ export default defineComponent({
os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
};
const { onMouseover: onReactionMouseover, onMouseleave: onReactionMouseleave } = useTooltip((showing) => {
useTooltip(reactionRef, (showing) => {
os.popup(XReactionTooltip, {
showing,
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
@@ -174,8 +170,6 @@ export default defineComponent({
rejectFollowRequest,
acceptGroupInvitation,
rejectGroupInvitation,
onReactionMouseover,
onReactionMouseleave,
elRef,
reactionRef,
};

View File

@@ -74,7 +74,7 @@ import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete';
import { noteVisibilities } from 'misskey-js';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { selectFiles } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
@@ -456,7 +456,7 @@ export default defineComponent({
},
chooseFileFrom(ev) {
selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
for (const file of files) {
this.files.push(file);
}

View File

@@ -6,10 +6,6 @@
class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()"
@touchstart.passive="onMouseover"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
@touchend="onMouseleave"
>
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
<span>{{ count }}</span>
@@ -90,7 +86,7 @@ export default defineComponent({
if (!props.isInitial) anime();
});
const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
useTooltip(buttonRef, async (showing) => {
const reactions = await os.api('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
@@ -113,8 +109,6 @@ export default defineComponent({
buttonRef,
canToggle,
toggleReaction,
onMouseover,
onMouseleave,
};
},
});

View File

@@ -3,10 +3,6 @@
ref="buttonRef"
class="eddddedb _button canRenote"
@click="renote()"
@touchstart.passive="onMouseover"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
@touchend="onMouseleave"
>
<i class="fas fa-retweet"></i>
<p v-if="count > 0" class="count">{{ count }}</p>
@@ -42,7 +38,7 @@ export default defineComponent({
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
useTooltip(buttonRef, async (showing) => {
const renotes = await os.api('notes/renotes', {
noteId: props.note.id,
limit: 11
@@ -87,8 +83,6 @@ export default defineComponent({
buttonRef,
canRenote,
renote,
onMouseover,
onMouseleave,
};
},
});

View File

@@ -1,6 +1,6 @@
<template>
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')">
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ fixed, top: position === 'top' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<slot :max-height="maxHeight" :close="close"></slot>
</div>
</transition>
@@ -8,6 +8,7 @@
<script lang="ts">
import { defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue';
import * as os from '@/os';
function getFixedContainer(el: Element | null | undefined): Element | null {
if (el == null || el.tagName === 'BODY') return null;
@@ -57,6 +58,7 @@ export default defineComponent({
const transformOrigin = ref('center');
const showing = ref(true);
const content = ref<HTMLElement>();
const zIndex = os.claimZIndex(props.front);
const close = () => {
// eslint-disable-next-line vue/no-mutating-props
@@ -204,6 +206,7 @@ export default defineComponent({
transformOrigin,
maxHeight,
close,
zIndex,
};
},
});
@@ -226,14 +229,9 @@ export default defineComponent({
.ccczpooj {
position: absolute;
z-index: 10000;
&.fixed {
position: fixed;
}
&.front {
z-index: 20000;
}
}
</style>

View File

@@ -53,6 +53,7 @@ export default defineComponent({
> .title {
opacity: 0.7;
margin: 0 0 8px 0;
font-size: 0.9em;
}
> .items {
@@ -63,6 +64,7 @@ export default defineComponent({
box-sizing: border-box;
padding: 10px 16px 10px 8px;
border-radius: 9px;
font-size: 0.95em;
&:hover {
text-decoration: none;

View File

@@ -1,6 +1,6 @@
<template>
<transition name="tooltip" appear @after-leave="$emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot>
</div>
</transition>
@@ -8,6 +8,7 @@
<script lang="ts">
import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue';
import * as os from '@/os';
export default defineComponent({
props: {
@@ -33,6 +34,7 @@ export default defineComponent({
setup(props, context) {
const el = ref<HTMLElement>();
const zIndex = os.claimZIndex(true);
const setPosition = () => {
if (el.value == null) return;
@@ -88,6 +90,7 @@ export default defineComponent({
return {
el,
zIndex,
};
},
})
@@ -108,7 +111,6 @@ export default defineComponent({
.buebdbiu {
position: absolute;
z-index: 11000;
font-size: 0.8em;
padding: 8px 12px;
box-sizing: border-box;

View File

@@ -1,6 +1,6 @@
<template>
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
<div v-if="showing" class="ebkgocck" :class="{ front }">
<div v-if="showing" class="ebkgocck">
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left">
@@ -124,10 +124,6 @@ export default defineComponent({
this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
os.windows.set(this.id, {
z: Number(document.defaultView.getComputedStyle(this.$el, null).zIndex)
});
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
this.top();
@@ -135,7 +131,6 @@ export default defineComponent({
},
unmounted() {
os.windows.delete(this.id);
window.removeEventListener('resize', this.onBrowserResize);
},
@@ -160,17 +155,7 @@ export default defineComponent({
// 最前面へ移動
top() {
let z = 0;
const ws = Array.from(os.windows.entries()).filter(([k, v]) => k !== this.id).map(([k, v]) => v);
for (const w of ws) {
if (w.z > z) z = w.z;
}
if (z > 0) {
(this.$el as any).style.zIndex = z + 1;
os.windows.set(this.id, {
z: z + 1
});
}
(this.$el as any).style.zIndex = os.claimZIndex(this.front);
},
onBodyMousedown() {
@@ -394,11 +379,6 @@ export default defineComponent({
position: fixed;
top: 0;
left: 0;
z-index: 10000; // mk-modalのと同じでなければならない
&.front {
z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
}
> .body {
overflow: hidden;

View File

@@ -2,11 +2,11 @@
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
import { Directive, ref } from 'vue';
import { isDeviceTouch } from '@/scripts/is-device-touch';
import { isTouchUsing } from '@/scripts/touch';
import { popup, alert } from '@/os';
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
const start = isTouchUsing ? 'touchstart' : 'mouseover';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
const delay = 100;
export default {

View File

@@ -12,24 +12,12 @@ import { resolve } from '@/router';
import { $i } from '@/account';
import { defaultStore } from '@/store';
export let isScreenTouching = false;
window.addEventListener('touchstart', () => {
isScreenTouching = true;
}, { passive: true });
window.addEventListener('touchend', () => {
isScreenTouching = false;
}, { passive: true });
export const stream = markRaw(new Misskey.Stream(url, $i));
export const pendingApiRequestsCount = ref(0);
let apiRequestsCount = 0; // for debug
export const apiRequests = ref([]); // for debug
export const windows = new Map();
const apiClient = new Misskey.api.APIClient({
origin: url,
});
@@ -174,6 +162,18 @@ export const popups = ref([]) as Ref<{
props: Record<string, any>;
}[]>;
let popupZIndex = 1000000;
let popupZIndexForFront = 2000000;
export function claimZIndex(front = false): number {
if (front) {
popupZIndexForFront += 100;
return popupZIndexForFront;
} else {
popupZIndex += 100;
return popupZIndex;
}
}
export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) {
if (component.then) component = await component;
@@ -182,7 +182,6 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
const id = ++popupIdCount;
const dispose = () => {
if (_DEV_) console.log('os:popup close', id, component, props, events);
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
setTimeout(() => {
popups.value = popups.value.filter(popup => popup.id !== id);
@@ -198,7 +197,6 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
id,
};
if (_DEV_) console.log('os:popup open', id, component, props, events);
popups.value.push(state);
return {

View File

@@ -67,60 +67,82 @@ import { physics } from '@/scripts/physics';
import * as symbols from '@/symbols';
const patrons = [
'Satsuki Yanagi',
'noellabo',
'まっちゃとーにゅ',
'mametsuko',
'noellabo',
'AureoleArk',
'Gargron',
'Nokotaro Takeda',
'Suji Yan',
'Hekovic',
'Gitmo Life Services',
'nenohi',
'naga_rus',
'Melilot',
'Efertone',
'oi_yekssim',
'nanami kan',
'regtan',
'Hekovic',
'nenohi',
'Gitmo Life Services',
'naga_rus',
'Efertone',
'Melilot',
'motcha',
'dansup',
'nanami kan',
'sevvie Rose',
'Hayato Ishikawa',
'Puniko',
'skehmatics',
'Quinton Macejkovic',
'YUKIMOCHI',
'dansup',
'mewl hayabusa',
'Emilis',
'Fristi',
'makokunsan',
'chidori ninokura',
'Peter G.',
'Nesakko',
'regtan',
'見当かなみ',
'natalie',
'Jerry',
'Maronu',
'Steffen K9',
'takimura',
'sikyosyounin',
'Nesakko',
'YuzuRyo61',
'blackskye',
'sheeta.s',
'osapon',
'mkatze',
'public_yusuke',
'CG',
'吴浥',
't_w',
'Jerry',
'nafuchoco',
'Takumi Sugita',
'chidori ninokura',
'mydarkstar',
'kiritan',
'GLaTAN',
'mkatze',
'kabo2468y',
'weepjp',
'Liaizon Wakest',
'Steffen K9',
'mydarkstar',
'Roujo',
'DignifiedSilence',
'uroco @99',
'totokoro',
'public_yusuke',
'うし',
'kiritan',
'weepjp',
'Liaizon Wakest',
'Duponin',
'Blue',
'Naoki Hirayama',
'wara',
'S Y',
'Wataru Manji (manji0)',
'みなしま',
'kanoy',
'xianon',
'Denshi',
'Osushimaru',
'吴浥',
'DignifiedSilence',
't_w',
'にょんへら',
'おのだい',
'Leni',
'oss',
'Weeble',
'蝉暮せせせ',
];
export default defineComponent({

View File

@@ -1,19 +1,20 @@
<template>
<div class="uqshojas">
<section v-for="ad in ads" class="_card _gap ads">
<div class="_content ad">
<MkSpacer :content-max="900">
<div class="uqshojas">
<div v-for="ad in ads" class="_panel _formRoot ad">
<MkAd v-if="ad.url" :specify="ad"/>
<MkInput v-model="ad.url" type="url">
<MkInput v-model="ad.url" type="url" class="_formBlock">
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl">
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ $ts.imageUrl }}</template>
</MkInput>
<div style="margin: 32px 0;">
<MkRadio v-model="ad.place" value="square">square</MkRadio>
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
</div>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
<option value="square">square</option>
<option value="horizontal">horizontal</option>
<option value="horizontal-big">horizontal-big</option>
</FormRadios>
<!--
<div style="margin: 32px 0;">
{{ $ts.priority }}
@@ -22,22 +23,24 @@
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
</div>
-->
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ $ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ $ts.expiration }}</template>
</MkInput>
<MkTextarea v-model="ad.memo">
<div class="_inputSplit">
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ $ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ $ts.expiration }}</template>
</MkInput>
</div>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ $ts.memo }}</template>
</MkTextarea>
<div class="buttons">
<MkButton class="button" inline primary @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
</div>
</div>
</section>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@@ -45,7 +48,7 @@ import { defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkRadio from '@/components/form/radio.vue';
import FormRadios from '@/components/form/radios.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -54,7 +57,7 @@ export default defineComponent({
MkButton,
MkInput,
MkTextarea,
MkRadio,
FormRadios,
},
emits: ['info'],
@@ -132,6 +135,12 @@ export default defineComponent({
<style lang="scss" scoped>
.uqshojas {
margin: var(--margin);
> .ad {
padding: 32px;
&:not(:last-child) {
margin-bottom: var(--margin);
}
}
}
</style>

View File

@@ -1,50 +1,54 @@
<template>
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkPagination ref="emojis" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
</div>
</button>
</div>
</template>
</MkPagination>
</div>
<MkSpacer :content-max="900">
<div class="ogwlenmc">
<div v-if="tab === 'local'" class="local">
<MkInput v-model="query" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkPagination ref="emojis" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.category }}</div>
</div>
</button>
</div>
</template>
</MkPagination>
</div>
<div v-else-if="tab === 'remote'" class="remote">
<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
<template #label>{{ $ts.host }}</template>
</MkInput>
<MkPagination ref="remoteEmojis" :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
<div v-else-if="tab === 'remote'" class="remote">
<div class="_inputSplit">
<MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
<MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
<MkPagination ref="remoteEmojis" :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
<div class="info">{{ emoji.host }}</div>
</div>
</div>
</div>
</div>
</template>
</MkPagination>
</template>
</MkPagination>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@@ -53,7 +57,7 @@ import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue';
import { selectFile } from '@/scripts/select-file';
import { selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -78,6 +82,9 @@ export default defineComponent({
icon: 'fas fa-plus',
text: this.$ts.addEmoji,
handler: this.add,
}, {
icon: 'fas fa-ellipsis-h',
handler: this.menu,
}],
tabs: [{
active: this.tab === 'local',
@@ -117,7 +124,7 @@ export default defineComponent({
methods: {
async add(e) {
const files = await selectFile(e.currentTarget || e.target, null, true);
const files = await selectFiles(e.currentTarget || e.target, null);
const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
fileId: file.id,
@@ -160,6 +167,28 @@ export default defineComponent({
icon: 'fas fa-plus',
action: () => { this.im(emoji) }
}], ev.currentTarget || ev.target);
},
menu(ev) {
os.popupMenu([{
icon: 'fas fa-download',
text: this.$ts.export,
action: async () => {
os.api('export-custom-emojis', {
})
.then(() => {
os.alert({
type: 'info',
text: this.$ts.exportRequested,
});
}).catch((e) => {
os.alert({
type: 'error',
text: e.message,
});
});
}
}], ev.currentTarget || ev.target);
}
}
});
@@ -168,15 +197,15 @@ export default defineComponent({
<style lang="scss" scoped>
.ogwlenmc {
> .local {
.empty {
margin: var(--margin);
.empty {
margin: var(--margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
margin: var(--margin);
margin: var(--margin) 0;
> .emoji {
display: flex;
@@ -214,15 +243,15 @@ export default defineComponent({
}
> .remote {
.empty {
margin: var(--margin);
}
.empty {
margin: var(--margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
margin: var(--margin);
margin: var(--margin) 0;
> .emoji {
display: flex;

View File

@@ -6,7 +6,7 @@
</FormSwitch>
<template v-if="enableDiscordIntegration">
<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
<FormInput v-model="discordClientId">
<template #prefix><i class="fas fa-key"></i></template>
@@ -67,6 +67,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
this.uri = meta.uri;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret;

View File

@@ -6,7 +6,7 @@
</FormSwitch>
<template v-if="enableGithubIntegration">
<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
<FormInput v-model="githubClientId">
<template #prefix><i class="fas fa-key"></i></template>
@@ -67,6 +67,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
this.uri = meta.uri;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.githubClientId = meta.githubClientId;
this.githubClientSecret = meta.githubClientSecret;

View File

@@ -6,7 +6,7 @@
</FormSwitch>
<template v-if="enableTwitterIntegration">
<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
<FormInput v-model="twitterConsumerKey">
<template #prefix><i class="fas fa-key"></i></template>
@@ -67,6 +67,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
this.uri = meta.uri;
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.twitterConsumerKey = meta.twitterConsumerKey;
this.twitterConsumerSecret = meta.twitterConsumerSecret;

View File

@@ -67,7 +67,7 @@ export default defineComponent({
send() {
this.sending = true;
const body = JSON5.parse(this.body);
os.api(this.endpoint, body, body.i || this.withCredential ? undefined : null).then(res => {
os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => {
this.sending = false;
this.res = JSON5.stringify(res, null, 2);
}, err => {

View File

@@ -112,7 +112,7 @@ export default defineComponent({
},
setBannerImage(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
selectFile(e.currentTarget || e.target, null).then(file => {
this.bannerId = file.id;
});
},

View File

@@ -1,16 +1,18 @@
<template>
<div v-if="clip" class="_section">
<div class="okzinsic _content _panel _gap">
<div v-if="clip.description" class="description">
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
<MkSpacer :content-max="800">
<div v-if="clip">
<div class="okzinsic _panel">
<div v-if="clip.description" class="description">
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
</div>
<div class="user">
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
<div class="user">
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</div>
<XNotes class="_content _gap" :pagination="pagination" :detail="true"/>
</div>
<XNotes :pagination="pagination" :detail="true"/>
</div>
</MkSpacer>
</template>
<script lang="ts">
@@ -40,10 +42,11 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => this.clip ? {
title: this.clip.name,
icon: 'fas fa-paperclip',
action: {
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-ellipsis-h',
handler: this.menu
}
}],
} : null),
clip: null,
pagination: {
@@ -133,6 +136,7 @@ export default defineComponent({
<style lang="scss" scoped>
.okzinsic {
position: relative;
margin-bottom: var(--margin);
> .description {
padding: 16px;

View File

@@ -20,6 +20,8 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
icon: 'fas fa-cloud',
bg: 'var(--bg)',
hideHeader: true,
},
folder: null,
};

View File

@@ -21,10 +21,38 @@ export default defineComponent({
title: this.$ts.customEmojis,
icon: 'fas fa-laugh',
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-ellipsis-h',
handler: this.menu
}],
})),
tab: 'category',
}
},
methods: {
menu(ev) {
os.popupMenu([{
icon: 'fas fa-download',
text: this.$ts.export,
action: async () => {
os.api('export-custom-emojis', {
})
.then(() => {
os.alert({
type: 'info',
text: this.$ts.exportRequested,
});
}).catch((e) => {
os.alert({
type: 'error',
text: e.message,
});
});
}
}], ev.currentTarget || ev.target);
}
}
});
</script>

View File

@@ -37,7 +37,7 @@ import FormTuple from '@/components/debobigego/tuple.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormGroup from '@/components/debobigego/group.vue';
import FormSuspense from '@/components/debobigego/suspense.vue';
import { selectFile } from '@/scripts/select-file';
import { selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -95,7 +95,7 @@ export default defineComponent({
methods: {
selectFile(e) {
selectFile(e.currentTarget || e.target, null, true).then(files => {
selectFiles(e.currentTarget || e.target, null).then(files => {
this.files = this.files.concat(files);
});
},

View File

@@ -152,7 +152,7 @@ export default defineComponent({
},
chooseFile(e) {
selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => {
selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => {
this.file = file;
});
},

View File

@@ -448,7 +448,7 @@ export default defineComponent({
},
setEyeCatchingImage(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
selectFile(e.currentTarget || e.target, null).then(file => {
this.eyeCatchingImageId = file.id;
});
},

View File

@@ -210,7 +210,7 @@ export default defineComponent({
},
chooseImage(key, e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
selectFile(e.currentTarget || e.target, null).then(file => {
room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
this.$refs.preview.selected(room.getSelectedObject());
this.changed = true;

View File

@@ -2,106 +2,158 @@
<div class="_formRoot">
<FormSection>
<template #label>{{ $ts._exportOrImport.allNotes }}</template>
<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="exportNotes()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.followingList }}</template>
<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
<FormGroup>
<FormSwitch v-model="excludeMutingUsers" class="_formBlock">
{{ $ts._exportOrImport.excludeMutingUsers }}
</FormSwitch>
<FormSwitch v-model="excludeInactiveUsers" class="_formBlock">
{{ $ts._exportOrImport.excludeInactiveUsers }}
</FormSwitch>
<MkButton :class="$style.button" inline @click="exportFollowing()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
</FormGroup>
<FormGroup>
<MkButton :class="$style.button" inline @click="importFollowing($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormGroup>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.userLists }}</template>
<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
<MkButton :class="$style.button" inline @click="exportUserLists()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="importUserLists($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.muteList }}</template>
<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
<MkButton :class="$style.button" inline @click="exportMuting()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="importMuting($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
<FormSection>
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
<MkButton :class="$style.button" inline @click="exportBlocking()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
<MkButton :class="$style.button" inline @click="importBlocking($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
</FormSection>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, onMounted, ref } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormSection from '@/components/form/section.vue';
import FormGroup from '@/components/form/group.vue';
import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
FormGroup,
FormSwitch,
MkButton,
},
emits: ['info'],
data() {
setup(props, context) {
const INFO = {
title: i18n.locale.importAndExport,
icon: 'fas fa-boxes',
bg: 'var(--bg)',
};
const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false);
const onExportSuccess = () => {
os.alert({
type: 'info',
text: i18n.locale.exportRequested,
});
};
const onImportSuccess = () => {
os.alert({
type: 'info',
text: i18n.locale.importRequested,
});
};
const onError = (e) => {
os.alert({
type: 'error',
text: e.message,
});
};
const exportNotes = () => {
os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
};
const exportFollowing = () => {
os.api('i/export-following', {
excludeMuting: excludeMutingUsers.value,
excludeInactive: excludeInactiveUsers.value,
})
.then(onExportSuccess).catch(onError);
};
const exportBlocking = () => {
os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError);
};
const exportUserLists = () => {
os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
};
const exportMuting = () => {
os.api('i/export-mute', {}).then(onExportSuccess).catch(onError);
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget || ev.target);
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget || ev.target);
os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget || ev.target);
os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget || ev.target);
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
onMounted(() => {
context.emit('info', INFO);
});
return {
[symbols.PAGE_INFO]: {
title: this.$ts.importAndExport,
icon: 'fas fa-boxes',
bg: 'var(--bg)',
},
}
[symbols.PAGE_INFO]: INFO,
excludeMutingUsers,
excludeInactiveUsers,
exportNotes,
exportFollowing,
exportBlocking,
exportUserLists,
exportMuting,
importFollowing,
importUserLists,
importMuting,
importBlocking,
};
},
mounted() {
this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
doExport(target) {
os.api(
target === 'notes' ? 'i/export-notes' :
target === 'following' ? 'i/export-following' :
target === 'blocking' ? 'i/export-blocking' :
target === 'user-lists' ? 'i/export-user-lists' :
target === 'muting' ? 'i/export-mute' :
null, {})
.then(() => {
os.alert({
type: 'info',
text: this.$ts.exportRequested
});
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message
});
});
},
async doImport(target, e) {
const file = await selectFile(e.currentTarget || e.target);
os.api(
target === 'following' ? 'i/import-following' :
target === 'user-lists' ? 'i/import-user-lists' :
target === 'muting' ? 'i/import-muting' :
target === 'blocking' ? 'i/import-blocking' :
null, {
fileId: file.id
}).then(() => {
os.alert({
type: 'info',
text: this.$ts.importRequested
});
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message
});
});
},
}
});
</script>

View File

@@ -1,23 +1,25 @@
<template>
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div v-if="!narrow || page == null" class="nav">
<MkSpacer :content-max="700" :margin-min="20">
<div class="baaadecd">
<div class="title">{{ $ts.settings }}</div>
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
<div class="header">
<div class="title">{{ $ts.settings }}</div>
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div>
</div>
<div class="body">
<div v-if="!narrow || page == null" class="nav">
<div class="baaadecd">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
</div>
</MkSpacer>
</div>
<div class="main">
<MkSpacer :content-max="600" :margin-min="20">
<div class="bkzroven">
<div v-if="childInfo" class="title">{{ childInfo.title }}</div>
<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
<div class="main">
<div class="bkzroven">
<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
</div>
</div>
</MkSpacer>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@@ -136,6 +138,11 @@ export default defineComponent({
text: i18n.locale.importAndExport,
to: '/settings/import-export',
active: page.value === 'import-export',
}, {
icon: 'fas fa-volume-mute',
text: i18n.locale.instanceMute,
to: '/settings/instance-mute',
active: page.value === 'instance-mute',
}, {
icon: 'fas fa-ban',
text: i18n.locale.muteAndBlock,
@@ -190,6 +197,7 @@ export default defineComponent({
case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
case 'instance-mute': return defineAsyncComponent(() => import('./instance-mute.vue'));
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
@@ -286,66 +294,62 @@ export default defineComponent({
<style lang="scss" scoped>
.vvcocwet {
> .nav {
.baaadecd {
> .title {
margin: 16px;
font-size: 1.5em;
font-weight: bold;
}
> .info {
margin: 16px 0;
}
> .accounts {
> .avatar {
display: block;
width: 50px;
height: 50px;
margin: 8px auto 16px auto;
}
}
}
}
> .main {
.bkzroven {
> .title {
margin: 4px 0 20px 0;
font-size: 1.5em;
font-weight: bold;
}
}
}
&.wide {
> .header {
display: flex;
max-width: 1000px;
margin: 0 auto;
height: 100%;
margin-bottom: 24px;
font-size: 1.3em;
font-weight: bold;
> .title {
width: 34%;
}
> .subtitle {
flex: 1;
min-width: 0;
}
}
> .body {
> .nav {
width: 32%;
box-sizing: border-box;
overflow: auto;
.baaadecd {
> .title {
margin: 24px 0;
> .info {
margin: 16px 0;
}
> .accounts {
> .avatar {
display: block;
width: 50px;
height: 50px;
margin: 8px auto 16px auto;
}
}
}
}
> .main {
flex: 1;
min-width: 0;
overflow: auto;
.bkzroven {
> .title {
margin: 6px 0 24px 0;
}
}
}
}
&.wide {
> .body {
display: flex;
height: 100%;
> .nav {
width: 34%;
padding-right: 32px;
box-sizing: border-box;
overflow: auto;
}
> .main {
flex: 1;
min-width: 0;
overflow: auto;
}
}
}

View File

@@ -0,0 +1,72 @@
<template>
<div class="_formRoot">
<MkInfo>{{ $ts._instanceMute.title }}</MkInfo>
<FormTextarea v-model="instanceMutes" class="_formBlock">
<template #label>{{ $ts._instanceMute.heading }}</template>
<template #caption>{{ $ts._instanceMute.instanceMuteDescription }}<br>{{ $ts._instanceMute.instanceMuteDescription2 }}</template>
</FormTextarea>
<MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import MkInfo from '@/components/ui/info.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkButton,
FormTextarea,
MkInfo,
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceMute,
icon: 'fas fa-volume-mute'
},
tab: 'soft',
instanceMutes: '',
changed: false,
}
},
watch: {
instanceMutes: {
handler() {
this.changed = true;
},
deep: true
},
},
mounted() {
this.$emit('info', this[symbols.PAGE_INFO]);
},
async created() {
this.instanceMutes = this.$i.mutedInstances.join('\n');
},
methods: {
async save() {
let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el);
await os.api('i/update', {
mutedInstances: mutes,
});
this.changed = false;
// Refresh filtered list to signal to the user how they've been saved
this.instanceMutes = mutes.join('\n');
},
}
})
</script>

View File

@@ -1,42 +1,41 @@
<template>
<FormBase>
<FormLink to="/settings/update">Misskey Update</FormLink>
<div class="_formRoot">
<FormLink to="/settings/update" class="_formBlock">Misskey Update</FormLink>
<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote" class="_formBlock">
{{ $ts.showFeaturedNotesInTimeline }}
</FormSwitch>
<FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
<FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
<FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink>
<FormLink to="/settings/experimental-features" class="_formBlock">{{ $ts.experimentalFeatures }}</FormLink>
<FormGroup>
<FormSection>
<template #label>{{ $ts.developer }}</template>
<FormSwitch v-model="debug" @update:modelValue="changeDebug">
<FormSwitch v-model="debug" @update:modelValue="changeDebug" class="_formBlock">
DEBUG MODE
</FormSwitch>
<template v-if="debug">
<FormButton @click="taskmanager">Task Manager</FormButton>
</template>
</FormGroup>
</FormSection>
<FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
<FormLink to="/settings/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
<FormLink to="/bios" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
<FormLink to="/cli" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
<FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
</FormBase>
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/debobigego/link.vue';
import FormBase from '@/components/debobigego/base.vue';
import FormGroup from '@/components/debobigego/group.vue';
import FormButton from '@/components/debobigego/button.vue';
import * as os from '@/os';
import { debug } from '@/config';
@@ -46,12 +45,11 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
FormBase,
FormSelect,
FormSection,
FormSwitch,
FormButton,
FormLink,
FormGroup,
},
emits: ['info'],

View File

@@ -188,7 +188,7 @@ export default defineComponent({
themesCount,
wallpaper,
setWallpaper(e) {
selectFile(e.currentTarget || e.target, null, false).then(file => {
selectFile(e.currentTarget || e.target, null).then(file => {
wallpaper.value = file.url;
});
},

View File

@@ -1 +0,0 @@
export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;

View File

@@ -1,8 +1,9 @@
import * as os from '@/os';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { DriveFile } from 'misskey-js/built/entities';
export function selectFile(src: any, label: string | null, multiple = false) {
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
return new Promise((res, rej) => {
const chooseFileFromPc = () => {
const input = document.createElement('input');
@@ -86,3 +87,11 @@ export function selectFile(src: any, label: string | null, multiple = false) {
}], src);
});
}
export function selectFile(src: any, label: string | null = null): Promise<DriveFile> {
return select(src, label, false) as Promise<DriveFile>;
}
export function selectFiles(src: any, label: string | null = null): Promise<DriveFile[]> {
return select(src, label, true) as Promise<DriveFile[]>;
}

View File

@@ -0,0 +1,19 @@
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
export let isTouchUsing = false;
export let isScreenTouching = false;
if (isTouchSupported) {
window.addEventListener('touchstart', () => {
// maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも
// タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする
isTouchUsing = true;
isScreenTouching = true;
}, { passive: true });
window.addEventListener('touchend', () => {
isScreenTouching = false;
}, { passive: true });
}

View File

@@ -1,9 +1,16 @@
import { isScreenTouching } from '@/os';
import { Ref, ref } from 'vue';
import { isDeviceTouch } from './is-device-touch';
import { Ref, ref, watch } from 'vue';
export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
export function useTooltip(
elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
onShow: (showing: Ref<boolean>) => void,
): void {
let isHovering = false;
// iOS(Androidも)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ
// 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる
// TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...
let shouldIgnoreMouseover = false;
let timeoutId: number;
let changeShowingState: (() => void) | null;
@@ -12,10 +19,6 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
close();
if (!isHovering) return;
// iOS(Androidも)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、その対策
// これが無いと、画面に触れてないのにツールチップが出たりしてしまう
if (isDeviceTouch && !isScreenTouching) return;
const showing = ref(true);
onShow(showing);
changeShowingState = () => {
@@ -32,6 +35,7 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
const onMouseover = () => {
if (isHovering) return;
if (shouldIgnoreMouseover) return;
isHovering = true;
timeoutId = window.setTimeout(open, 300);
};
@@ -43,8 +47,31 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
close();
};
return {
onMouseover,
onMouseleave,
const onTouchstart = () => {
shouldIgnoreMouseover = true;
if (isHovering) return;
isHovering = true;
timeoutId = window.setTimeout(open, 300);
};
const onTouchend = () => {
if (!isHovering) return;
isHovering = false;
window.clearTimeout(timeoutId);
close();
};
const stop = watch(elRef, () => {
if (elRef.value) {
stop();
const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
el.addEventListener('mouseover', onMouseover, { passive: true });
el.addEventListener('mouseleave', onMouseleave, { passive: true });
el.addEventListener('touchstart', onTouchstart, { passive: true });
el.addEventListener('touchend', onTouchend, { passive: true });
}
}, {
immediate: true,
flush: 'post',
});
}

View File

@@ -346,7 +346,7 @@ hr {
._popup {
background: var(--popup);
border-radius: var(--radius);
contain: layout; // ふき出しがボックスから飛び出て表示されるようなデザインをする場合もあるので paint は contain することができない
contain: contain;
}
// TODO: 廃止

View File

@@ -59,7 +59,7 @@ import * as Acct from 'misskey-js/built/acct';
import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete';
import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { selectFiles } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce';
@@ -342,7 +342,7 @@ export default defineComponent({
},
chooseFileFrom(ev) {
selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
for (const file of files) {
this.files.push(file);
}

File diff suppressed because it is too large Load Diff