Merge branch 'notification-read-api' into swn

This commit is contained in:
tamaina
2022-01-21 21:30:11 +09:00
446 changed files with 7250 additions and 7678 deletions

View File

@@ -13,7 +13,7 @@
"@discordapp/twemoji": "13.1.0",
"@syuilo/aiscript": "0.11.1",
"@types/dateformat": "3.0.1",
"@types/escape-regexp": "0.0.0",
"@types/escape-regexp": "0.0.1",
"@types/glob": "7.2.0",
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
@@ -31,14 +31,14 @@
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.3",
"@types/uuid": "8.3.4",
"@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.2",
"@typescript-eslint/parser": "5.8.1",
"@vue/compiler-sfc": "3.2.26",
"@typescript-eslint/parser": "5.10.0",
"@vue/compiler-sfc": "3.2.28",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "5.0.1",
@@ -48,28 +48,28 @@
"chart.js": "3.7.0",
"chartjs-adapter-date-fns": "2.0.0",
"chartjs-plugin-zoom": "1.2.0",
"compare-versions": "4.1.2",
"compare-versions": "4.1.3",
"content-disposition": "0.5.4",
"crc-32": "1.2.0",
"css-loader": "6.5.1",
"cssnano": "5.0.14",
"cssnano": "5.0.15",
"date-fns": "2.28.0",
"escape-regexp": "0.0.1",
"eslint": "8.6.0",
"eslint-plugin-vue": "8.2.0",
"eslint": "8.7.0",
"eslint-plugin-vue": "8.3.0",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
"glob": "7.2.0",
"idb-keyval": "6.0.3",
"idb-keyval": "6.1.0",
"insert-text-at-cursor": "0.3.0",
"ip-cidr": "3.0.4",
"json5": "2.2.0",
"json5-loader": "4.0.1",
"katex": "0.15.1",
"katex": "0.15.2",
"langmap": "0.0.16",
"matter-js": "0.18.0",
"mfm-js": "0.20.0",
"misskey-js": "0.0.12",
"mfm-js": "0.21.0",
"misskey-js": "0.0.13",
"mocha": "8.4.0",
"ms": "2.1.3",
"nested-property": "4.0.0",
@@ -78,7 +78,7 @@
"portscanner": "2.2.0",
"postcss": "8.4.5",
"postcss-loader": "6.2.1",
"prismjs": "1.25.0",
"prismjs": "1.26.0",
"private-ip": "2.3.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
@@ -90,7 +90,7 @@
"request-stats": "3.0.0",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.45.2",
"sass": "1.49.0",
"sass-loader": "12.4.0",
"seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0",
@@ -98,7 +98,7 @@
"style-loader": "3.3.1",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.117.1",
"three": "0.136.0",
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
@@ -107,11 +107,11 @@
"tsc-alias": "1.5.0",
"tsconfig-paths": "3.12.0",
"twemoji-parser": "13.1.0",
"typescript": "4.5.4",
"typescript": "4.5.5",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vue": "3.2.26",
"vue": "3.2.28",
"vue-loader": "17.0.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.5",
@@ -119,18 +119,18 @@
"vue-svg-loader": "0.17.0-beta.2",
"vuedraggable": "4.0.1",
"web-push": "3.4.5",
"webpack": "5.65.0",
"webpack": "5.66.0",
"webpack-cli": "4.9.1",
"websocket": "1.0.34",
"ws": "8.4.0"
"ws": "8.4.2"
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.54",
"@types/fluent-ffmpeg": "2.1.17",
"@typescript-eslint/eslint-plugin": "5.8.1",
"@redocly/openapi-core": "1.0.0-beta.79",
"@types/fluent-ffmpeg": "2.1.20",
"@typescript-eslint/eslint-plugin": "5.10.0",
"cross-env": "7.0.3",
"cypress": "9.2.0",
"eslint-plugin-import": "2.25.3",
"cypress": "9.3.1",
"eslint-plugin-import": "2.25.4",
"start-server-and-test": "1.14.0"
}
}

View File

@@ -16,6 +16,8 @@ const data = localStorage.getItem('account');
// TODO: 外部からはreadonlyに
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
export async function signout() {
waiting();
localStorage.removeItem('account');
@@ -127,7 +129,12 @@ export async function login(token: Account['token'], redirect?: string) {
unisonReload();
}
export async function openAccountMenu(ev: MouseEvent) {
export async function openAccountMenu(opts: {
includeCurrentAccount?: boolean;
withExtraOperation: boolean;
active?: misskey.entities.UserDetailed['id'];
onChoose?: (account: misskey.entities.UserDetailed) => void;
}, ev: MouseEvent) {
function showSigninDialog() {
popup(import('@/components/signin-dialog.vue'), {}, {
done: res => {
@@ -146,7 +153,7 @@ export async function openAccountMenu(ev: MouseEvent) {
}, 'closed');
}
async function switchAccount(account: any) {
async function switchAccount(account: misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token;
switchAccountWithToken(token);
@@ -159,48 +166,58 @@ export async function openAccountMenu(ev: MouseEvent) {
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
function createItem(account: misskey.entities.UserDetailed) {
return {
type: 'user',
user: account,
active: opts.active != null ? opts.active === account.id : false,
action: () => {
if (opts.onChoose) {
opts.onChoose(account);
} else {
switchAccount(account);
}
},
};
}
const accountItemPromises = storedAccounts.map(a => new Promise(res => {
accountsPromise.then(accounts => {
const account = accounts.find(x => x.id === a.id);
if (account == null) return res(null);
res({
type: 'user',
user: account,
action: () => { switchAccount(account); }
});
res(createItem(account));
});
}));
popupMenu([...[{
type: 'link',
text: i18n.locale.profile,
to: `/@${ $i.username }`,
avatar: $i,
}, null, ...accountItemPromises, {
icon: 'fas fa-plus',
text: i18n.locale.addAccount,
action: () => {
popupMenu([{
text: i18n.locale.existingAccount,
action: () => { showSigninDialog(); },
}, {
text: i18n.locale.createAccount,
action: () => { createAccount(); },
}], ev.currentTarget || ev.target);
},
}, {
type: 'link',
icon: 'fas fa-users',
text: i18n.locale.manageAccounts,
to: `/settings/accounts`,
}]], ev.currentTarget || ev.target, {
align: 'left'
});
}
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$i: typeof $i;
if (opts.withExtraOperation) {
popupMenu([...[{
type: 'link',
text: i18n.locale.profile,
to: `/@${ $i.username }`,
avatar: $i,
}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
icon: 'fas fa-plus',
text: i18n.locale.addAccount,
action: () => {
popupMenu([{
text: i18n.locale.existingAccount,
action: () => { showSigninDialog(); },
}, {
text: i18n.locale.createAccount,
action: () => { createAccount(); },
}], ev.currentTarget || ev.target);
},
}, {
type: 'link',
icon: 'fas fa-users',
text: i18n.locale.manageAccounts,
to: `/settings/accounts`,
}]], ev.currentTarget || ev.target, {
align: 'left'
});
} else {
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, {
align: 'left'
});
}
}

View File

@@ -0,0 +1,102 @@
<template>
<div class="bcekxzvu _card _gap">
<div class="_content target">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
</MkA>
</div>
<div class="_content">
<div>
<Mfm :text="report.comment"/>
</div>
<hr/>
<div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div>
<div v-if="report.assignee">
{{ $ts.moderator }}:
<MkAcct :user="report.assignee"/>
</div>
<div><MkTime :time="report.createdAt"/></div>
</div>
<div class="_footer">
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
{{ $ts.forwardReport }}
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
</MkSwitch>
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import { acct, userPage } from '@/filters/user';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
},
emits: ['resolved'],
props: {
report: {
type: Object,
required: true,
}
}
data() {
return {
forward: this.report.forwarded,
};
}
methods: {
acct,
userPage,
resolve() {
os.apiWithDialog('admin/resolve-abuse-user-report', {
forward: this.forward,
reportId: this.report.id,
}).then(() => {
this.$emit('resolved', this.report.id);
});
}
}
});
</script>
<style lang="scss" scoped>
.bcekxzvu {
> .target {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
> .avatar {
width: 42px;
height: 42px;
}
> .info {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
> .name {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -55,12 +55,10 @@ const variable = computed(() => {
const loaded = computed(() => !!window[variable.value]);
const src = computed(() => {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
recaptcha: 'https://www.recaptcha.net/recaptcha',
} as Record<CaptchaProvider, string>)[props.provider];
return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
switch (props.provider) {
case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
}
});
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);

View File

@@ -1,5 +1,5 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')">
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
<div class="mk-dialog">
<div v-if="icon" class="icon">
<i :class="icon"></i>
@@ -28,8 +28,8 @@
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton>
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
</div>
<div v-if="actions" class="buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
@@ -44,6 +44,7 @@ import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import { i18n } from '@/i18n';
type Input = {
type: HTMLInputElement['type'];

View File

@@ -7,10 +7,10 @@
@click="cancel()"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
@closed="emit('closed')"
>
<template #header>
{{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
@@ -23,6 +23,7 @@ import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import number from '@/filters/number';
import { i18n } from '@/i18n';
withDefaults(defineProps<{
type?: 'file' | 'folder';

View File

@@ -3,10 +3,10 @@
:initial-width="800"
:initial-height="500"
:can-resize="true"
@closed="$emit('closed')"
@closed="emit('closed')"
>
<template #header>
{{ $ts.drive }}
{{ i18n.locale.drive }}
</template>
<XDrive :initial-folder="initialFolder"/>
</XWindow>
@@ -17,12 +17,13 @@ import { } from 'vue';
import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XWindow from '@/components/ui/window.vue';
import { i18n } from '@/i18n';
defineProps<{
initialFolder?: Misskey.entities.DriveFolder;
}>();
defineEmits<{
const emit = defineEmits<{
(e: 'closed'): void;
}>();
</script>

View File

@@ -8,17 +8,17 @@
@dragstart="onDragstart"
@dragend="onDragend"
>
<div v-if="$i.avatarId == file.id" class="label">
<div v-if="$i?.avatarId == file.id" class="label">
<img src="/client-assets/label.svg"/>
<p>{{ $ts.avatar }}</p>
<p>{{ i18n.locale.avatar }}</p>
</div>
<div v-if="$i.bannerId == file.id" class="label">
<div v-if="$i?.bannerId == file.id" class="label">
<img src="/client-assets/label.svg"/>
<p>{{ $ts.banner }}</p>
<p>{{ i18n.locale.banner }}</p>
</div>
<div v-if="file.isSensitive" class="label red">
<img src="/client-assets/label-red.svg"/>
<p>{{ $ts.nsfw }}</p>
<p>{{ i18n.locale.nsfw }}</p>
</div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -30,179 +30,155 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { $i } from '@/account';
export default defineComponent({
components: {
MkDriveFileThumbnail
},
props: {
file: {
type: Object,
required: true,
},
isSelected: {
type: Boolean,
required: false,
default: false,
},
selectMode: {
type: Boolean,
required: false,
default: false,
}
},
emits: ['chosen'],
data() {
return {
isDragging: false
};
},
computed: {
// TODO: parentへの参照を無くす
browser(): any {
return this.$parent;
},
title(): string {
return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
}
},
methods: {
getMenu() {
return [{
text: this.$ts.rename,
icon: 'fas fa-i-cursor',
action: this.rename
}, {
text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: this.toggleSensitive
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: this.describe
}, null, {
text: this.$ts.copyUrl,
icon: 'fas fa-link',
action: this.copyUrl
}, {
type: 'a',
href: this.file.url,
target: '_blank',
text: this.$ts.download,
icon: 'fas fa-download',
download: this.file.name
}, null, {
text: this.$ts.delete,
icon: 'fas fa-trash-alt',
danger: true,
action: this.deleteFile
}];
},
onClick(ev) {
if (this.selectMode) {
this.$emit('chosen', this.file);
} else {
os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
}
},
onContextmenu(e) {
os.contextMenu(this.getMenu(), e);
},
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
this.isDragging = true;
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
// (=あなたの子供が、ドラッグを開始しましたよ)
this.browser.isDragSource = true;
},
onDragend(e) {
this.isDragging = false;
this.browser.isDragSource = false;
},
rename() {
os.inputText({
title: this.$ts.renameFile,
placeholder: this.$ts.inputNewFileName,
default: this.file.name,
allowEmpty: false
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/files/update', {
fileId: this.file.id,
name: name
});
});
},
describe() {
os.popup(import('@/components/media-caption.vue'), {
title: this.$ts.describeFile,
input: {
placeholder: this.$ts.inputNewDescription,
default: this.file.comment !== null ? this.file.comment : '',
},
image: this.file
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result;
os.api('drive/files/update', {
fileId: this.file.id,
comment: comment.length == 0 ? null : comment
});
}
}, 'closed');
},
toggleSensitive() {
os.api('drive/files/update', {
fileId: this.file.id,
isSensitive: !this.file.isSensitive
});
},
copyUrl() {
copyToClipboard(this.file.url);
os.success();
},
addApp() {
alert('not implemented yet');
},
async deleteFile() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
});
if (canceled) return;
os.api('drive/files/delete', {
fileId: this.file.id
});
},
bytes
}
const props = withDefaults(defineProps<{
file: Misskey.entities.DriveFile;
isSelected?: boolean;
selectMode?: boolean;
}>(), {
isSelected: false,
selectMode: false,
});
const emit = defineEmits<{
(e: 'chosen', r: Misskey.entities.DriveFile): void;
(e: 'dragstart'): void;
(e: 'dragend'): void;
}>();
const isDragging = ref(false);
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
function getMenu() {
return [{
text: i18n.locale.rename,
icon: 'fas fa-i-cursor',
action: rename
}, {
text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
action: toggleSensitive
}, {
text: i18n.locale.describeFile,
icon: 'fas fa-i-cursor',
action: describe
}, null, {
text: i18n.locale.copyUrl,
icon: 'fas fa-link',
action: copyUrl
}, {
type: 'a',
href: props.file.url,
target: '_blank',
text: i18n.locale.download,
icon: 'fas fa-download',
download: props.file.name
}, null, {
text: i18n.locale.delete,
icon: 'fas fa-trash-alt',
danger: true,
action: deleteFile
}];
}
function onClick(ev: MouseEvent) {
if (props.selectMode) {
emit('chosen', props.file);
} else {
os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
}
}
function onContextmenu(e: MouseEvent) {
os.contextMenu(getMenu(), e);
}
function onDragstart(e: DragEvent) {
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
}
isDragging.value = true;
emit('dragstart');
}
function onDragend() {
isDragging.value = false;
emit('dragend');
}
function rename() {
os.inputText({
title: i18n.locale.renameFile,
placeholder: i18n.locale.inputNewFileName,
default: props.file.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/files/update', {
fileId: props.file.id,
name: name
});
});
}
function describe() {
os.popup(import('@/components/media-caption.vue'), {
title: i18n.locale.describeFile,
input: {
placeholder: i18n.locale.inputNewDescription,
default: props.file.comment !== null ? props.file.comment : '',
},
image: props.file
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result;
os.api('drive/files/update', {
fileId: props.file.id,
comment: comment.length == 0 ? null : comment
});
}
}, 'closed');
}
function toggleSensitive() {
os.api('drive/files/update', {
fileId: props.file.id,
isSensitive: !props.file.isSensitive
});
}
function copyUrl() {
copyToClipboard(props.file.url);
os.success();
}
/*
function addApp() {
alert('not implemented yet');
}
*/
async function deleteFile() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
});
if (canceled) return;
os.api('drive/files/delete', {
fileId: props.file.id
});
}
</script>
<style lang="scss" scoped>

View File

@@ -19,243 +19,233 @@
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
{{ folder.name }}
</p>
<p v-if="$store.state.uploadFolder == folder.id" class="upload">
{{ $ts.uploadFolder }}
<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
{{ i18n.locale.uploadFolder }}
</p>
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
export default defineComponent({
props: {
folder: {
type: Object,
required: true,
},
isSelected: {
type: Boolean,
required: false,
default: false,
},
selectMode: {
type: Boolean,
required: false,
default: false,
}
},
emits: ['chosen'],
data() {
return {
hover: false,
draghover: false,
isDragging: false,
};
},
computed: {
browser(): any {
return this.$parent;
},
title(): string {
return this.folder.name;
}
},
methods: {
checkboxClicked(e) {
this.$emit('chosen', this.folder);
},
onClick() {
this.browser.move(this.folder);
},
onMouseover() {
this.hover = true;
},
onMouseout() {
this.hover = false
},
onDragover(e) {
// 自分自身がドラッグされている場合
if (this.isDragging) {
// 自分自身にはドロップさせない
e.dataTransfer.dropEffect = 'none';
return;
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
}
},
onDragenter() {
if (!this.isDragging) this.draghover = true;
},
onDragleave() {
this.draghover = false;
},
onDrop(e) {
this.draghover = false;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) {
this.browser.upload(file, this.folder);
}
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.browser.removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: this.folder.id
});
}
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (folder.id == this.folder.id) return;
this.browser.removeFolder(folder.id);
os.api('drive/folders/update', {
folderId: folder.id,
parentId: this.folder.id
}).then(() => {
// noop
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
os.alert({
title: this.$ts.unableToProcess,
text: this.$ts.circularReferenceFolder
});
break;
default:
os.alert({
type: 'error',
text: this.$ts.somethingHappened
});
}
});
}
//#endregion
},
onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
this.isDragging = true;
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
// (=あなたの子供が、ドラッグを開始しましたよ)
this.browser.isDragSource = true;
},
onDragend() {
this.isDragging = false;
this.browser.isDragSource = false;
},
go() {
this.browser.move(this.folder.id);
},
newWindow() {
this.browser.newWindow(this.folder);
},
rename() {
os.inputText({
title: this.$ts.renameFolder,
placeholder: this.$ts.inputNewFolderName,
default: this.folder.name
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/update', {
folderId: this.folder.id,
name: name
});
});
},
deleteFolder() {
os.api('drive/folders/delete', {
folderId: this.folder.id
}).then(() => {
if (this.$store.state.uploadFolder === this.folder.id) {
this.$store.set('uploadFolder', null);
}
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
title: this.$ts.unableToDelete,
text: this.$ts.hasChildFilesOrFolders
});
break;
default:
os.alert({
type: 'error',
text: this.$ts.unableToDelete
});
}
});
},
setAsUploadFolder() {
this.$store.set('uploadFolder', this.folder.id);
},
onContextmenu(e) {
os.contextMenu([{
text: this.$ts.openInWindow,
icon: 'fas fa-window-restore',
action: () => {
os.popup(import('./drive-window.vue'), {
initialFolder: this.folder
}, {
}, 'closed');
}
}, null, {
text: this.$ts.rename,
icon: 'fas fa-i-cursor',
action: this.rename
}, null, {
text: this.$ts.delete,
icon: 'fas fa-trash-alt',
danger: true,
action: this.deleteFolder
}], e);
},
}
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
isSelected?: boolean;
selectMode?: boolean;
}>(), {
isSelected: false,
selectMode: false,
});
const emit = defineEmits<{
(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
(ev: 'move', v: Misskey.entities.DriveFolder): void;
(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
(ev: 'dragstart'): void;
(ev: 'dragend'): void;
}>();
const hover = ref(false);
const draghover = ref(false);
const isDragging = ref(false);
const title = computed(() => props.folder.name);
function checkboxClicked() {
emit('chosen', props.folder);
}
function onClick() {
emit('move', props.folder);
}
function onMouseover() {
hover.value = true;
}
function onMouseout() {
hover.value = false
}
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
// 自分自身がドラッグされている場合
if (isDragging.value) {
// 自分自身にはドロップさせない
ev.dataTransfer.dropEffect = 'none';
return;
}
const isFile = ev.dataTransfer.items[0].kind == 'file';
const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else {
ev.dataTransfer.dropEffect = 'none';
}
}
function onDragenter() {
if (!isDragging.value) draghover.value = true;
}
function onDragleave() {
draghover.value = false;
}
function onDrop(ev: DragEvent) {
draghover.value = false;
if (!ev.dataTransfer) return;
// ファイルだったら
if (ev.dataTransfer.files.length > 0) {
for (const file of Array.from(ev.dataTransfer.files)) {
emit('upload', file, props.folder);
}
return;
}
//#region ドライブのファイル
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: props.folder.id
});
}
//#endregion
//#region ドライブのフォルダ
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (folder.id == props.folder.id) return;
emit('removeFolder', folder.id);
os.api('drive/folders/update', {
folderId: folder.id,
parentId: props.folder.id
}).then(() => {
// noop
}).catch(err => {
switch (err) {
case 'detected-circular-definition':
os.alert({
title: i18n.locale.unableToProcess,
text: i18n.locale.circularReferenceFolder
});
break;
default:
os.alert({
type: 'error',
text: i18n.locale.somethingHappened
});
}
});
}
//#endregion
}
function onDragstart(ev: DragEvent) {
if (!ev.dataTransfer) return;
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
isDragging.value = true;
// 親ブラウザに対して、ドラッグが開始されたフラグを立てる
// (=あなたの子供が、ドラッグを開始しましたよ)
emit('dragstart');
}
function onDragend() {
isDragging.value = false;
emit('dragend');
}
function go() {
emit('move', props.folder.id);
}
function rename() {
os.inputText({
title: i18n.locale.renameFolder,
placeholder: i18n.locale.inputNewFolderName,
default: props.folder.name
}).then(({ canceled, result: name }) => {
if (canceled) return;
os.api('drive/folders/update', {
folderId: props.folder.id,
name: name
});
});
}
function deleteFolder() {
os.api('drive/folders/delete', {
folderId: props.folder.id
}).then(() => {
if (defaultStore.state.uploadFolder === props.folder.id) {
defaultStore.set('uploadFolder', null);
}
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
title: i18n.locale.unableToDelete,
text: i18n.locale.hasChildFilesOrFolders
});
break;
default:
os.alert({
type: 'error',
text: i18n.locale.unableToDelete
});
}
});
}
function setAsUploadFolder() {
defaultStore.set('uploadFolder', props.folder.id);
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu([{
text: i18n.locale.openInWindow,
icon: 'fas fa-window-restore',
action: () => {
os.popup(import('./drive-window.vue'), {
initialFolder: props.folder
}, {
}, 'closed');
}
}, null, {
text: i18n.locale.rename,
icon: 'fas fa-i-cursor',
action: rename,
}, null, {
text: i18n.locale.delete,
icon: 'fas fa-trash-alt',
danger: true,
action: deleteFolder,
}], ev);
}
</script>
<style lang="scss" scoped>

View File

@@ -8,114 +8,111 @@
@drop.stop="onDrop"
>
<i v-if="folder == null" class="fas fa-cloud"></i>
<span>{{ folder == null ? $ts.drive : folder.name }}</span>
<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
props: {
folder: {
type: Object,
required: false,
}
},
const props = defineProps<{
folder?: Misskey.entities.DriveFolder;
parentFolder: Misskey.entities.DriveFolder | null;
}>();
data() {
return {
hover: false,
draghover: false,
};
},
const emit = defineEmits<{
(e: 'move', v?: Misskey.entities.DriveFolder): void;
(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
}>();
computed: {
browser(): any {
return this.$parent;
}
},
const hover = ref(false);
const draghover = ref(false);
methods: {
onClick() {
this.browser.move(this.folder);
},
function onClick() {
emit('move', props.folder);
}
onMouseover() {
this.hover = true;
},
function onMouseover() {
hover.value = true;
}
onMouseout() {
this.hover = false;
},
function onMouseout() {
hover.value = false;
}
onDragover(e) {
// このフォルダがルートかつカレントディレクトリならドロップ禁止
if (this.folder == null && this.browser.folder == null) {
e.dataTransfer.dropEffect = 'none';
}
function onDragover(e: DragEvent) {
if (!e.dataTransfer) return;
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
}
return false;
},
onDragenter() {
if (this.folder || this.browser.folder) this.draghover = true;
},
onDragleave() {
if (this.folder || this.browser.folder) this.draghover = false;
},
onDrop(e) {
this.draghover = false;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) {
this.browser.upload(file, this.folder);
}
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.browser.removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: this.folder ? this.folder.id : null
});
}
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (this.folder && folder.id == this.folder.id) return;
this.browser.removeFolder(folder.id);
os.api('drive/folders/update', {
folderId: folder.id,
parentId: this.folder ? this.folder.id : null
});
}
//#endregion
}
// このフォルダがルートかつカレントディレクトリならドロップ禁止
if (props.folder == null && props.parentFolder == null) {
e.dataTransfer.dropEffect = 'none';
}
});
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
}
return false;
}
function onDragenter() {
if (props.folder || props.parentFolder) draghover.value = true;
}
function onDragleave() {
if (props.folder || props.parentFolder) draghover.value = false;
}
function onDrop(e: DragEvent) {
draghover.value = false;
if (!e.dataTransfer) return;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) {
emit('upload', file, props.folder);
}
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
os.api('drive/files/update', {
fileId: file.id,
folderId: props.folder ? props.folder.id : null
});
}
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (props.folder && folder.id == props.folder.id) return;
emit('removeFolder', folder.id);
os.api('drive/folders/update', {
folderId: folder.id,
parentId: props.folder ? props.folder.id : null
});
}
//#endregion
}
</script>
<style lang="scss" scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +1,65 @@
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
<MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
<MkModal
ref="modal"
v-slot="{ type, maxHeight }"
:z-priority="'middle'"
:prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
:transparent-bg="true"
:manual-showing="manualShowing"
:src="src"
@click="modal?.close()"
@opening="opening"
@close="emit('close')"
@closed="emit('closed')"
>
<MkEmojiPicker
ref="picker"
class="ryghynhb _popup _shadow"
:class="{ drawer: type === 'drawer' }"
:show-pinned="showPinned"
:as-reaction-picker="asReactionPicker"
:as-drawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
/>
</MkModal>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
import { defaultStore } from '@/store';
export default defineComponent({
components: {
MkModal,
MkEmojiPicker,
},
props: {
manualShowing: {
type: Boolean,
required: false,
default: null,
},
src: {
required: false
},
showPinned: {
required: false,
default: true
},
asReactionPicker: {
required: false
},
},
emits: ['done', 'close', 'closed'],
data() {
return {
};
},
methods: {
chosen(emoji: any) {
this.$emit('done', emoji);
this.$refs.modal.close();
},
opening() {
this.$refs.picker.reset();
this.$refs.picker.focus();
}
}
withDefaults(defineProps<{
manualShowing?: boolean;
src?: HTMLElement;
showPinned?: boolean;
asReactionPicker?: boolean;
}>(), {
manualShowing: false,
showPinned: true,
asReactionPicker: false,
});
const emit = defineEmits<{
(e: 'done', v: any): void;
(e: 'close'): void;
(e: 'closed'): void;
}>();
const modal = ref<InstanceType<typeof MkModal>>();
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
function chosen(emoji: any) {
emit('done', emoji);
modal.value?.close();
}
function opening() {
picker.value?.reset();
picker.value?.focus();
}
</script>
<style lang="scss" scoped>

View File

@@ -5,50 +5,33 @@
:can-resize="false"
:mini="true"
:front="true"
@closed="$emit('closed')"
@closed="emit('closed')"
>
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
</MkWindow>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkWindow from '@/components/ui/window.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
export default defineComponent({
components: {
MkWindow,
MkEmojiPicker,
},
props: {
src: {
required: false
},
showPinned: {
required: false,
default: true
},
asReactionPicker: {
required: false
},
},
emits: ['chosen', 'closed'],
data() {
return {
};
},
methods: {
chosen(emoji: any) {
this.$emit('chosen', emoji);
},
}
withDefaults(defineProps<{
src?: HTMLElement;
showPinned?: boolean;
asReactionPicker?: boolean;
}>(), {
showPinned: true,
});
const emit = defineEmits<{
(e: 'chosen', v: any): void;
(e: 'closed'): void;
}>();
function chosen(emoji: any) {
emit('chosen', emoji);
}
</script>
<style lang="scss" scoped>

View File

@@ -7,7 +7,7 @@
<button v-for="emoji in emojis"
:key="emoji"
class="_button"
@click="chosen(emoji, $event)"
@click="emit('chosen', emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
</button>
@@ -15,35 +15,19 @@
</section>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
<script lang="ts" setup>
import { ref } from 'vue';
export default defineComponent({
props: {
emojis: {
required: true,
},
initialShown: {
required: false
}
},
const props = defineProps<{
emojis: string[];
initialShown?: boolean;
}>();
emits: ['chosen'],
const emit = defineEmits<{
(e: 'chosen', v: string, ev: MouseEvent): void;
}>();
data() {
return {
getStaticImageUrl,
shown: this.initialShown,
};
},
methods: {
chosen(emoji: any, ev) {
this.$parent.chosen(emoji, ev);
},
}
});
const shown = ref(!!props.initialShown);
</script>
<style lang="scss" scoped>

View File

@@ -1,18 +1,18 @@
<template>
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">
<button v-for="emoji in searchResultCustom"
:key="emoji"
:key="emoji.id"
class="_button"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
<img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0">
@@ -43,9 +43,9 @@
</section>
<section>
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
<div>
<button v-for="emoji in $store.state.recentlyUsedEmojis"
<button v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button"
@click="chosen(emoji, $event)"
@@ -56,12 +56,12 @@
</section>
</div>
<div>
<header class="_acrylic">{{ $ts.customEmojis }}</header>
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
<header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
</div>
<div>
<header class="_acrylic">{{ $ts.emoji }}</header>
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
<header class="_acrylic">{{ i18n.locale.emoji }}</header>
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -73,277 +73,272 @@
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import { emojilist } from '@/scripts/emojilist';
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Ripple from '@/components/ripple.vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { isMobile } from '@/scripts/is-mobile';
import { emojiCategories } from '@/instance';
import { emojiCategories, instance } from '@/instance';
import XSection from './emoji-picker.section.vue';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
export default defineComponent({
components: {
XSection
},
const props = withDefaults(defineProps<{
showPinned?: boolean;
asReactionPicker?: boolean;
maxHeight?: number;
asDrawer?: boolean;
}>(), {
showPinned: true,
});
props: {
showPinned: {
required: false,
default: true,
},
asReactionPicker: {
required: false,
},
maxHeight: {
type: Number,
required: false,
},
asDrawer: {
type: Boolean,
required: false
},
},
const emit = defineEmits<{
(e: 'chosen', v: string): void;
}>();
emits: ['chosen'],
const search = ref<HTMLInputElement>();
const emojis = ref<HTMLDivElement>();
data() {
return {
emojilist: markRaw(emojilist),
getStaticImageUrl,
pinned: this.$store.reactiveState.reactions,
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
big: this.asReactionPicker ? isTouchUsing : false,
customEmojiCategories: emojiCategories,
customEmojis: this.$instance.emojis,
q: null,
searchResultCustom: [],
searchResultUnicode: [],
tab: 'index',
categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
};
},
const {
reactions: pinned,
reactionPickerWidth,
reactionPickerHeight,
disableShowingAnimatedImages,
recentlyUsedEmojis,
} = defaultStore.reactiveState;
watch: {
q() {
this.$refs.emojis.scrollTop = 0;
const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
const big = props.asReactionPicker ? isTouchUsing : false;
const customEmojiCategories = emojiCategories;
const customEmojis = instance.emojis;
const q = ref<string | null>(null);
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
if (this.q == null || this.q === '') {
this.searchResultCustom = [];
this.searchResultUnicode = [];
return;
}
watch(q, () => {
if (emojis.value) emojis.value.scrollTop = 0;
const q = this.q.replace(/:/g, '');
const searchCustom = () => {
const max = 8;
const emojis = this.customEmojis;
const matches = new Set();
const exactMatch = emojis.find(e => e.name === q);
if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND検索
const keywords = q.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
const searchUnicode = () => {
const max = 8;
const emojis = this.emojilist;
const matches = new Set();
const exactMatch = emojis.find(e => e.name === q);
if (exactMatch) matches.add(exactMatch);
if (q.includes(' ')) { // AND検索
const keywords = q.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(q)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(q))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
this.searchResultCustom = Array.from(searchCustom());
this.searchResultUnicode = Array.from(searchUnicode());
}
},
mounted() {
this.focus();
},
methods: {
focus() {
if (!isMobile && !isTouchUsing) {
this.$refs.search.focus({
preventScroll: true
});
}
},
reset() {
this.$refs.emojis.scrollTop = 0;
this.q = '';
},
getKey(emoji: any) {
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
},
chosen(emoji: any, ev) {
if (ev) {
const el = ev.currentTarget || ev.target;
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
const key = this.getKey(emoji);
this.$emit('chosen', key);
// 最近使った絵文字更新
if (!this.pinned.includes(key)) {
let recents = this.$store.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== key);
recents.unshift(key);
this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
}
},
paste(event) {
const paste = (event.clipboardData || window.clipboardData).getData('text');
if (this.done(paste)) {
event.preventDefault();
}
},
done(query) {
if (query == null) query = this.q;
if (query == null) return;
const q = query.replace(/:/g, '');
const exactMatchCustom = this.customEmojis.find(e => e.name === q);
if (exactMatchCustom) {
this.chosen(exactMatchCustom);
return true;
}
const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
if (exactMatchUnicode) {
this.chosen(exactMatchUnicode);
return true;
}
if (this.searchResultCustom.length > 0) {
this.chosen(this.searchResultCustom[0]);
return true;
}
if (this.searchResultUnicode.length > 0) {
this.chosen(this.searchResultUnicode[0]);
return true;
}
},
if (q.value == null || q.value === '') {
searchResultCustom.value = [];
searchResultUnicode.value = [];
return;
}
const newQ = q.value.replace(/:/g, '');
const searchCustom = () => {
const max = 8;
const emojis = customEmojis;
const matches = new Set<Misskey.entities.CustomEmoji>();
const exactMatch = emojis.find(e => e.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND検索
const keywords = newQ.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
const searchUnicode = () => {
const max = 8;
const emojis = emojilist;
const matches = new Set<UnicodeEmojiDef>();
const exactMatch = emojis.find(e => e.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND検索
const keywords = newQ.split(' ');
// 名前にキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
// 名前またはエイリアスにキーワードが含まれている
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
searchResultCustom.value = Array.from(searchCustom());
searchResultUnicode.value = Array.from(searchUnicode());
});
function focus() {
if (!isMobile && !isTouchUsing) {
search.value?.focus({
preventScroll: true
});
}
}
function reset() {
if (emojis.value) emojis.value.scrollTop = 0;
q.value = '';
}
function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
}
function chosen(emoji: any, ev?: MouseEvent) {
const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
const key = getKey(emoji);
emit('chosen', key);
// 最近使った絵文字更新
if (!pinned.value.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
}
function paste(event: ClipboardEvent) {
const paste = (event.clipboardData || window.clipboardData).getData('text');
if (done(paste)) {
event.preventDefault();
}
}
function done(query?: any): boolean | void {
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.find(e => e.name === q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;
}
const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
if (exactMatchUnicode) {
chosen(exactMatchUnicode);
return true;
}
if (searchResultCustom.value.length > 0) {
chosen(searchResultCustom.value[0]);
return true;
}
if (searchResultUnicode.value.length > 0) {
chosen(searchResultUnicode.value[0]);
return true;
}
}
onMounted(() => {
focus();
});
defineExpose({
focus,
reset,
});
</script>

View File

@@ -2,25 +2,15 @@
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
export default defineComponent({
components: {
},
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
data() {
return {
meta: null,
};
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
},
os.api('meta', { detail: true }).then(gotMeta => {
meta.value = gotMeta;
});
</script>

View File

@@ -6,129 +6,110 @@
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
<span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合 -->
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
<span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
</template>
</template>
<template v-else>
<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template>
</button>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
import { i18n } from '@/i18n';
export default defineComponent({
props: {
user: {
type: Object,
required: true
},
full: {
type: Boolean,
required: false,
default: false,
},
large: {
type: Boolean,
required: false,
default: false,
},
},
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed,
full?: boolean,
large?: boolean,
}>(), {
full: false,
large: false,
});
data() {
return {
isFollowing: this.user.isFollowing,
hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
wait: false,
connection: null,
};
},
const isFollowing = ref(props.user.isFollowing);
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
const wait = ref(false);
const connection = stream.useChannel('main');
created() {
// 渡されたユーザー情報が不完全な場合
if (this.user.isFollowing == null) {
os.api('users/show', {
userId: this.user.id
}).then(u => {
this.isFollowing = u.isFollowing;
this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
});
}
},
if (props.user.isFollowing == null) {
os.api('users/show', {
userId: props.user.id
}).then(u => {
isFollowing.value = u.isFollowing;
hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
});
}
mounted() {
this.connection = markRaw(stream.useChannel('main'));
this.connection.on('follow', this.onFollowChange);
this.connection.on('unfollow', this.onFollowChange);
},
beforeUnmount() {
this.connection.dispose();
},
methods: {
onFollowChange(user) {
if (user.id == this.user.id) {
this.isFollowing = user.isFollowing;
this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
},
async onClick() {
this.wait = true;
try {
if (this.isFollowing) {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
});
if (canceled) return;
await os.api('following/delete', {
userId: this.user.id
});
} else {
if (this.hasPendingFollowRequestFromYou) {
await os.api('following/requests/cancel', {
userId: this.user.id
});
} else if (this.user.isLocked) {
await os.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;
} else {
await os.api('following/create', {
userId: this.user.id
});
this.hasPendingFollowRequestFromYou = true;
}
}
} catch (e) {
console.error(e);
} finally {
this.wait = false;
}
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id == props.user.id) {
isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
}
}
async function onClick() {
wait.value = true;
try {
if (isFollowing.value) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
});
if (canceled) return;
await os.api('following/delete', {
userId: props.user.id
});
} else {
if (hasPendingFollowRequestFromYou.value) {
await os.api('following/requests/cancel', {
userId: props.user.id
});
} else if (props.user.isLocked) {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
} else {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
}
}
} catch (e) {
console.error(e);
} finally {
wait.value = false;
}
}
onMounted(() => {
connection.on('follow', onFollowChange);
connection.on('unfollow', onFollowChange);
});
onBeforeUnmount(() => {
connection.dispose();
});
</script>

View File

@@ -2,72 +2,64 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="$refs.dialog.close()"
@closed="$emit('closed')"
@close="dialog.close()"
@closed="emit('closed')"
>
<template #header>{{ $ts.forgotPassword }}</template>
<template #header>{{ i18n.locale.forgotPassword }}</template>
<form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<div class="main _formRoot">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
<template #label>{{ $ts.username }}</template>
<template #label>{{ i18n.locale.username }}</template>
<template #prefix>@</template>
</MkInput>
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
<template #label>{{ $ts.emailAddress }}</template>
<template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
<template #label>{{ i18n.locale.emailAddress }}</template>
<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
</MkInput>
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
</div>
<div class="sub">
<MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
<div v-else>
{{ $ts._forgotPassword.contactAdmin }}
<div v-else class="bafecedb">
{{ i18n.locale._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
const emit = defineEmits<{
(e: 'done'): void;
(e: 'closed'): void;
}>();
emits: ['done', 'closed'],
let dialog: InstanceType<typeof XModalWindow> = $ref();
data() {
return {
username: '',
email: '',
processing: false,
};
},
let username = $ref('');
let email = $ref('');
let processing = $ref(false);
methods: {
async onSubmit() {
this.processing = true;
await os.apiWithDialog('request-reset-password', {
username: this.username,
email: this.email,
});
this.$emit('done');
this.$refs.dialog.close();
}
}
});
async function onSubmit() {
processing = true;
await os.apiWithDialog('request-reset-password', {
username,
email,
});
emit('done');
dialog.close();
}
</script>
<style lang="scss" scoped>
@@ -81,4 +73,8 @@ export default defineComponent({
padding: 24px;
}
}
.bafecedb {
padding: 24px;
}
</style>

View File

@@ -35,9 +35,9 @@ const emit = defineEmits<{
(e: 'click', ev: MouseEvent): void;
}>();
const url = defaultStore.state.disableShowingAnimatedImages
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatarUrl)
: props.user.avatarUrl;
: props.user.avatarUrl);
function onClick(ev: MouseEvent) {
emit('click', ev);

View File

@@ -153,8 +153,8 @@ export default defineComponent({
this.$refs.window.close();
},
onContextmenu(e) {
os.contextMenu(this.contextmenu, e);
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev);
}
},
});

View File

@@ -4,6 +4,7 @@
v-show="!isDeleted"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
ref="el"
class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
@@ -222,21 +223,21 @@ function undoReact(note): void {
});
}
function onContextmenu(e): void {
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
e.preventDefault();
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
}
}

View File

@@ -211,21 +211,21 @@ function undoReact(note): void {
});
}
function onContextmenu(e): void {
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
e.preventDefault();
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
}
}

View File

@@ -32,9 +32,7 @@ const props = defineProps<{
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
defineExpose({
prepend: (note) => {
pagingComponent.value?.prepend(note);
},
pagingComponent,
});
</script>

View File

@@ -10,7 +10,7 @@
</div>
</template>
</XDraggable>
<p class="remain">{{ 4 - files.length }}/4</p>
<p class="remain">{{ 16 - files.length }}/16</p>
</div>
</template>
@@ -41,7 +41,6 @@ export default defineComponent({
data() {
return {
menu: null as Promise<null> | null,
};
},
@@ -99,10 +98,12 @@ export default defineComponent({
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result;
let comment = result.result.length == 0 ? null : result.result;
os.api('drive/files/update', {
fileId: file.id,
comment: comment.length == 0 ? null : comment
comment: comment,
}).then(() => {
file.comment = comment;
});
}
}, 'closed');

View File

@@ -8,25 +8,28 @@
>
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/>
</button>
<div>
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
<button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button>
<button v-tooltip="$ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div>
</header>
<div class="form" :class="{ fixed }">
<XNoteSimple v-if="reply" class="preview" :note="reply"/>
<XNoteSimple v-if="renote" class="preview" :note="renote"/>
<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
<div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id">
<MkAcct :user="u"/>
@@ -35,21 +38,21 @@
<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer>
<button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
<button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
<button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
<button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
<button v-tooltip="$ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
<button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
</footer>
<datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -83,7 +86,7 @@ import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { $i } from '@/account';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
const modal = inject('modal');
@@ -339,8 +342,8 @@ function focus() {
}
function chooseFileFrom(ev) {
selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files => {
for (const file of files) {
selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
for (const file of files_) {
files.push(file);
}
});
@@ -350,8 +353,8 @@ function detachFile(id) {
files = files.filter(x => x.id != id);
}
function updateFiles(files) {
files = files;
function updateFiles(_files) {
files = _files;
}
function updateFileSensitive(file, sensitive) {
@@ -553,8 +556,15 @@ async function post() {
}
}
let token = undefined;
if (postAccount) {
const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.id)?.token;
}
posting = true;
os.api('notes/create', data).then(() => {
os.api('notes/create', data, token).then(() => {
clear();
nextTick(() => {
deleteDraft();
@@ -565,6 +575,7 @@ async function post() {
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
posting = false;
postAccount = null;
});
}).catch(err => {
posting = false;
@@ -585,7 +596,7 @@ function insertMention() {
});
}
async function insertEmoji(ev) {
async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
}
@@ -602,6 +613,23 @@ function showActions(ev) {
})), ev.currentTarget || ev.target);
}
let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
function openAccountMenu(ev: MouseEvent) {
openAccountMenu_({
withExtraOperation: false,
includeCurrentAccount: true,
active: postAccount != null ? postAccount.id : $i.id,
onChoose: (account) => {
if (account.id === $i.id) {
postAccount = null;
} else {
postAccount = account;
}
},
}, ev);
}
onMounted(() => {
if (props.autofocus) {
focus();
@@ -678,6 +706,19 @@ onMounted(() => {
line-height: 66px;
}
> .account {
height: 100%;
aspect-ratio: 1/1;
display: inline-flex;
vertical-align: bottom;
> .avatar {
width: 28px;
height: 28px;
margin: auto;
}
}
> div {
position: absolute;
top: 0;

View File

@@ -25,10 +25,10 @@ const emit = defineEmits<{
provide('inChannel', computed(() => props.src === 'channel'));
const tlComponent = ref<InstanceType<typeof XNotes>>();
const tlComponent: InstanceType<typeof XNotes> = $ref();
const prepend = note => {
tlComponent.value.prepend(note);
tlComponent.pagingComponent?.prepend(note);
emit('note');
@@ -38,16 +38,16 @@ const prepend = note => {
};
const onUserAdded = () => {
tlComponent.value.reload();
tlComponent.pagingComponent?.reload();
};
const onUserRemoved = () => {
tlComponent.value.reload();
tlComponent.pagingComponent?.reload();
};
const onChangeFollowing = () => {
if (!tlComponent.value.backed) {
tlComponent.value.reload();
if (!tlComponent.pagingComponent?.backed) {
tlComponent.pagingComponent?.reload();
}
};

View File

@@ -24,7 +24,7 @@
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</a>
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" @click="clicked(item.action, $event)">
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>

View File

@@ -73,12 +73,11 @@ const queue = ref<Item[]>([]);
const offset = ref(0);
const fetching = ref(true);
const moreFetching = ref(false);
const inited = ref(false);
const more = ref(false);
const backed = ref(false); // 遡り中か否か
const isBackTop = ref(false);
const empty = computed(() => items.value.length === 0 && !fetching.value && inited.value);
const error = computed(() => !fetching.value && !inited.value);
const empty = computed(() => items.value.length === 0);
const error = ref(false);
const init = async (): Promise<void> => {
queue.value = [];
@@ -105,9 +104,10 @@ const init = async (): Promise<void> => {
more.value = false;
}
offset.value = res.length;
inited.value = true;
error.value = false;
fetching.value = false;
}, e => {
error.value = true;
fetching.value = false;
});
};
@@ -183,30 +183,36 @@ const fetchMoreAhead = async (): Promise<void> => {
};
const prepend = (item: Item): void => {
if (rootEl.value == null) return;
if (props.pagination.reversed) {
const container = getScrollContainer(rootEl.value);
if (container == null) return; // TODO?
if (rootEl.value) {
const container = getScrollContainer(rootEl.value);
if (container == null) return; // TODO?
const pos = getScrollPosition(rootEl.value);
const viewHeight = container.clientHeight;
const height = container.scrollHeight;
const isBottom = (pos + viewHeight > height - 32);
if (isBottom) {
// オーバーフローしたら古いアイテムは捨てる
if (items.value.length >= props.displayLimit) {
// このやり方だとVue 3.2以降アニメーションが動かなくなる
//items.value = items.value.slice(-props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.shift();
const pos = getScrollPosition(rootEl.value);
const viewHeight = container.clientHeight;
const height = container.scrollHeight;
const isBottom = (pos + viewHeight > height - 32);
if (isBottom) {
// オーバーフローしたら古いアイテムは捨てる
if (items.value.length >= props.displayLimit) {
// このやり方だとVue 3.2以降アニメーションが動かなくなる
//items.value = items.value.slice(-props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.shift();
}
more.value = true;
}
more.value = true;
}
}
items.value.push(item);
// TODO
} else {
// 初回表示時はunshiftだけでOK
if (!rootEl.value) {
items.value.unshift(item);
return;
}
const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
if (isTop) {
@@ -265,6 +271,7 @@ onDeactivated(() => {
defineExpose({
items,
queue,
backed,
reload,
fetchMoreAhead,
prepend,

View File

@@ -147,9 +147,9 @@ export default defineComponent({
}
},
onContextmenu(e) {
onContextmenu(ev: MouseEvent) {
if (this.contextmenu) {
os.contextMenu(this.contextmenu, e);
os.contextMenu(this.contextmenu, ev);
}
},

View File

@@ -541,7 +541,7 @@ export const uploads = ref<{
img: string;
}[]>([]);
export function upload(file: File, folder?: any, name?: string) {
export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => {

View File

@@ -3,36 +3,39 @@
<MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz">
<div id="debug"></div>
<div ref="about" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div>
<div class="_formBlock" style="text-align: center;">
{{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a>
{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a>
</div>
<div class="_formBlock" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>
<div class="_formLinks">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="fas fa-code"></i></template>
{{ $ts._aboutMisskey.source }}
{{ i18n.locale._aboutMisskey.source }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="fas fa-language"></i></template>
{{ $ts._aboutMisskey.translation }}
{{ i18n.locale._aboutMisskey.translation }}
<template #suffix>Crowdin</template>
</FormLink>
<FormLink to="https://www.patreon.com/syuilo" external>
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
{{ $ts._aboutMisskey.donate }}
{{ i18n.locale._aboutMisskey.donate }}
<template #suffix>Patreon</template>
</FormLink>
</div>
</FormSection>
<FormSection>
<template #label>{{ $ts._aboutMisskey.contributors }}</template>
<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template>
<div class="_formLinks">
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
@@ -44,27 +47,30 @@
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
</div>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template>
</FormSection>
<FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
<template #caption>{{ $ts._aboutMisskey.morePatrons }}</template>
<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template>
</FormSection>
</div>
</MkSpacer>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { nextTick, onBeforeUnmount } from 'vue';
import { version } from '@/config';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue';
import MkButton from '@/components/ui/button.vue';
import MkLink from '@/components/link.vue';
import { physics } from '@/scripts/physics';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import * as os from '@/os';
const patrons = [
'まっちゃとーにゅ',
@@ -145,58 +151,52 @@ const patrons = [
'蝉暮せせせ',
];
export default defineComponent({
components: {
FormSection,
FormLink,
MkKeyValue,
MkLink,
},
let easterEggReady = false;
let easterEggEmojis = $ref([]);
let easterEggEngine = $ref(null);
const containerEl = $ref<HTMLElement>();
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.aboutMisskey,
icon: null
},
version,
patrons,
easterEggReady: false,
easterEggEmojis: [],
easterEggEngine: null,
}
},
beforeUnmount() {
if (this.easterEggEngine) {
this.easterEggEngine.stop();
}
},
methods: {
iconLoaded() {
const emojis = this.$store.state.reactions;
const containerWidth = this.$refs.about.offsetWidth;
for (let i = 0; i < 32; i++) {
this.easterEggEmojis.push({
id: i.toString(),
top: -(128 + (Math.random() * 256)),
left: (Math.random() * containerWidth),
emoji: emojis[Math.floor(Math.random() * emojis.length)],
});
}
this.$nextTick(() => {
this.easterEggReady = true;
});
},
gravity() {
if (!this.easterEggReady) return;
this.easterEggReady = false;
this.easterEggEngine = physics(this.$refs.about);
}
function iconLoaded() {
const emojis = defaultStore.state.reactions;
const containerWidth = containerEl.offsetWidth;
for (let i = 0; i < 32; i++) {
easterEggEmojis.push({
id: i.toString(),
top: -(128 + (Math.random() * 256)),
left: (Math.random() * containerWidth),
emoji: emojis[Math.floor(Math.random() * emojis.length)],
});
}
nextTick(() => {
easterEggReady = true;
});
}
function gravity() {
if (!easterEggReady) return;
easterEggReady = false;
easterEggEngine = physics(containerEl);
}
function iLoveMisskey() {
os.post({
initialText: 'I $[jelly ❤] #Misskey',
});
}
onBeforeUnmount(() => {
if (easterEggEngine) {
easterEggEngine.stop();
}
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.locale.aboutMisskey,
icon: null,
bg: 'var(--bg)',
},
});
</script>

View File

@@ -34,27 +34,7 @@
-->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
<div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap">
<div class="_content target">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
<div class="info">
<MkUserName class="name" :user="report.targetUser"/>
<div class="acct">@{{ acct(report.targetUser) }}</div>
</div>
</div>
<div class="_content">
<div>
<Mfm :text="report.comment"/>
</div>
<hr>
<div>Reporter: <MkAcct :user="report.reporter"/></div>
<div><MkTime :time="report.createdAt"/></div>
</div>
<div class="_footer">
<div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
<MkButton v-if="!report.resolved" primary @click="resolve(report)">{{ $ts.abuseMarkAsResolved }}</MkButton>
</div>
</div>
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</MkPagination>
</div>
</div>
@@ -64,20 +44,19 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
import { acct } from '@/filters/user';
import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSelect,
MkPagination,
XAbuseReport,
},
emits: ['info'],
@@ -107,14 +86,8 @@ export default defineComponent({
},
methods: {
acct,
resolve(report) {
os.apiWithDialog('admin/resolve-abuse-user-report', {
reportId: report.id,
}).then(() => {
this.$refs.reports.removeItem(item => item.id === report.id);
});
resolved(reportId) {
this.$refs.reports.removeItem(item => item.id === reportId);
},
}
});
@@ -124,29 +97,4 @@ export default defineComponent({
.lcixvhis {
margin: var(--margin);
}
.bcekxzvu {
> .target {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
> .avatar {
width: 42px;
height: 42px;
}
> .info {
margin-left: 0.3em;
padding: 0 8px;
flex: 1;
> .name {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -28,7 +28,7 @@
<FormSection v-if="iAmModerator">
<template #label>Moderation</template>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
</FormSection>
<FormSection>
@@ -104,15 +104,14 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import * as misskey from 'misskey-js';
import MkChart from '@/components/chart.vue';
import MkObjectView from '@/components/object-view.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormLink from '@/components/form/link.vue';
import MkLink from '@/components/link.vue';
import FormSection from '@/components/form/section.vue';
import FormButton from '@/components/ui/button.vue';
import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue';
import FormSwitch from '@/components/form/switch.vue';
@@ -120,87 +119,57 @@ import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
import { iAmModerator } from '@/account';
export default defineComponent({
components: {
FormTextarea,
MkObjectView,
FormButton,
FormLink,
FormSection,
FormSwitch,
MkKeyValue,
MkSelect,
MkChart,
MkLink,
const props = defineProps<{
host: string;
}>();
let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
let instance = $ref<misskey.entities.Instance | null>(null);
let suspended = $ref(false);
let isBlocked = $ref(false);
let chartSrc = $ref('instance-requests');
let chartSpan = $ref('hour');
async function fetch() {
meta = await os.api('meta', { detail: true });
instance = await os.api('federation/show-instance', {
host: props.host,
});
suspended = instance.isSuspended;
isBlocked = meta.blockedHosts.includes(instance.host);
}
async function toggleBlock(ev) {
if (meta == null) return;
await os.api('admin/update-meta', {
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host)
});
}
async function toggleSuspend(v) {
await os.api('admin/federation/update-instance', {
host: instance.host,
isSuspended: suspended,
});
}
fetch();
defineExpose({
[symbols.PAGE_INFO]: {
title: props.host,
icon: 'fas fa-info-circle',
bg: 'var(--bg)',
actions: [{
text: `https://${props.host}`,
icon: 'fas fa-external-link-alt',
handler: () => {
window.open(`https://${props.host}`, '_blank');
}
}],
},
props: {
host: {
type: String,
required: true
}
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.host,
icon: 'fas fa-info-circle',
bg: 'var(--bg)',
actions: [{
text: `https://${this.host}`,
icon: 'fas fa-external-link-alt',
handler: () => {
window.open(`https://${this.host}`, '_blank');
}
}],
},
instance: null,
suspended: false,
chartSrc: 'instance-requests',
chartSpan: 'hour',
}
},
computed: {
iAmModerator(): boolean {
return this.$i && (this.$i.isAdmin || this.$i.isModerator);
},
isBlocked() {
return this.instance && this.$instance && this.$instance.blockedHosts && this.$instance.blockedHosts.includes(this.instance.host);
}
},
mounted() {
this.fetch();
},
methods: {
number,
bytes,
async fetch() {
this.instance = await os.api('federation/show-instance', {
host: this.host
});
this.suspended = this.instance.isSuspended;
},
changeBlock(e) {
os.api('admin/update-meta', {
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
});
},
async toggleSuspend(v) {
await os.api('admin/federation/update-instance', {
host: this.instance.host,
isSuspended: this.suspended
});
},
}
});
</script>

View File

@@ -3,50 +3,50 @@
<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div class="avatar _acrylic">
<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
<MkButton primary class="avatarEdit" @click="changeAvatar">{{ $ts._profile.changeAvatar }}</MkButton>
<MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton>
</div>
<MkButton primary class="bannerEdit" @click="changeBanner">{{ $ts._profile.changeBanner }}</MkButton>
<MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton>
</div>
<FormInput v-model="name" :max="30" manual-save class="_formBlock">
<template #label>{{ $ts._profile.name }}</template>
<FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
<template #label>{{ i18n.locale._profile.name }}</template>
</FormInput>
<FormTextarea v-model="description" :max="500" tall manual-save class="_formBlock">
<template #label>{{ $ts._profile.description }}</template>
<template #caption>{{ $ts._profile.youCanIncludeHashtags }}</template>
<FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
<template #label>{{ i18n.locale._profile.description }}</template>
<template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template>
</FormTextarea>
<FormInput v-model="location" manual-save class="_formBlock">
<template #label>{{ $ts.location }}</template>
<FormInput v-model="profile.location" manual-save class="_formBlock">
<template #label>{{ i18n.locale.location }}</template>
<template #prefix><i class="fas fa-map-marker-alt"></i></template>
</FormInput>
<FormInput v-model="birthday" type="date" manual-save class="_formBlock">
<template #label>{{ $ts.birthday }}</template>
<FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
<template #label>{{ i18n.locale.birthday }}</template>
<template #prefix><i class="fas fa-birthday-cake"></i></template>
</FormInput>
<FormSelect v-model="lang" class="_formBlock">
<template #label>{{ $ts.language }}</template>
<FormSelect v-model="profile.lang" class="_formBlock">
<template #label>{{ i18n.locale.language }}</template>
<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
</FormSelect>
<FormSlot>
<MkButton @click="editMetadata">{{ $ts._profile.metadataEdit }}</MkButton>
<template #caption>{{ $ts._profile.metadataDescription }}</template>
<MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton>
<template #caption>{{ i18n.locale._profile.metadataDescription }}</template>
</FormSlot>
<FormSwitch v-model="isCat" class="_formBlock">{{ $ts.flagAsCat }}<template #caption>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch>
<FormSwitch v-model="isBot" class="_formBlock">{{ $ts.flagAsBot }}<template #caption>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch>
<FormSwitch v-model="alwaysMarkNsfw" class="_formBlock">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineComponent, reactive, watch } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@@ -57,194 +57,149 @@ import { host, langs } from '@/config';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
import { $i } from '@/account';
export default defineComponent({
components: {
MkButton,
FormInput,
FormTextarea,
FormSwitch,
FormSelect,
FormSlot,
const profile = reactive({
name: $i.name,
description: $i.description,
location: $i.location,
birthday: $i.birthday,
lang: $i.lang,
isBot: $i.isBot,
isCat: $i.isCat,
alwaysMarkNsfw: $i.alwaysMarkNsfw,
});
const additionalFields = reactive({
fieldName0: $i.fields[0] ? $i.fields[0].name : null,
fieldValue0: $i.fields[0] ? $i.fields[0].value : null,
fieldName1: $i.fields[1] ? $i.fields[1].name : null,
fieldValue1: $i.fields[1] ? $i.fields[1].value : null,
fieldName2: $i.fields[2] ? $i.fields[2].name : null,
fieldValue2: $i.fields[2] ? $i.fields[2].value : null,
fieldName3: $i.fields[3] ? $i.fields[3].name : null,
fieldValue3: $i.fields[3] ? $i.fields[3].value : null,
});
watch(() => profile, () => {
save();
}, {
deep: true,
});
function save() {
os.apiWithDialog('i/update', {
name: profile.name || null,
description: profile.description || null,
location: profile.location || null,
birthday: profile.birthday || null,
lang: profile.lang || null,
isBot: !!profile.isBot,
isCat: !!profile.isCat,
alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
});
}
function changeAvatar(ev) {
selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => {
const i = await os.apiWithDialog('i/update', {
avatarId: file.id,
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
});
}
function changeBanner(ev) {
selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => {
const i = await os.apiWithDialog('i/update', {
bannerId: file.id,
});
$i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl;
});
}
async function editMetadata() {
const { canceled, result } = await os.form(i18n.locale._profile.metadata, {
fieldName0: {
type: 'string',
label: i18n.locale._profile.metadataLabel + ' 1',
default: additionalFields.fieldName0,
},
fieldValue0: {
type: 'string',
label: i18n.locale._profile.metadataContent + ' 1',
default: additionalFields.fieldValue0,
},
fieldName1: {
type: 'string',
label: i18n.locale._profile.metadataLabel + ' 2',
default: additionalFields.fieldName1,
},
fieldValue1: {
type: 'string',
label: i18n.locale._profile.metadataContent + ' 2',
default: additionalFields.fieldValue1,
},
fieldName2: {
type: 'string',
label: i18n.locale._profile.metadataLabel + ' 3',
default: additionalFields.fieldName2,
},
fieldValue2: {
type: 'string',
label: i18n.locale._profile.metadataContent + ' 3',
default: additionalFields.fieldValue2,
},
fieldName3: {
type: 'string',
label: i18n.locale._profile.metadataLabel + ' 4',
default: additionalFields.fieldName3,
},
fieldValue3: {
type: 'string',
label: i18n.locale._profile.metadataContent + ' 4',
default: additionalFields.fieldValue3,
},
});
if (canceled) return;
additionalFields.fieldName0 = result.fieldName0;
additionalFields.fieldValue0 = result.fieldValue0;
additionalFields.fieldName1 = result.fieldName1;
additionalFields.fieldValue1 = result.fieldValue1;
additionalFields.fieldName2 = result.fieldName2;
additionalFields.fieldValue2 = result.fieldValue2;
additionalFields.fieldName3 = result.fieldName3;
additionalFields.fieldValue3 = result.fieldValue3;
const fields = [
{ name: additionalFields.fieldName0, value: additionalFields.fieldValue0 },
{ name: additionalFields.fieldName1, value: additionalFields.fieldValue1 },
{ name: additionalFields.fieldName2, value: additionalFields.fieldValue2 },
{ name: additionalFields.fieldName3, value: additionalFields.fieldValue3 },
];
os.api('i/update', {
fields,
}).then(i => {
os.success();
}).catch(err => {
os.alert({
type: 'error',
text: err.id
});
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.locale.profile,
icon: 'fas fa-user',
bg: 'var(--bg)',
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.profile,
icon: 'fas fa-user',
bg: 'var(--bg)',
},
host,
langs,
name: null,
description: null,
birthday: null,
lang: null,
location: null,
fieldName0: null,
fieldValue0: null,
fieldName1: null,
fieldValue1: null,
fieldName2: null,
fieldValue2: null,
fieldName3: null,
fieldValue3: null,
avatarId: null,
bannerId: null,
isBot: false,
isCat: false,
alwaysMarkNsfw: false,
saving: false,
}
},
created() {
this.name = this.$i.name;
this.description = this.$i.description;
this.location = this.$i.location;
this.birthday = this.$i.birthday;
this.lang = this.$i.lang;
this.avatarId = this.$i.avatarId;
this.bannerId = this.$i.bannerId;
this.isBot = this.$i.isBot;
this.isCat = this.$i.isCat;
this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
this.$watch('name', this.save);
this.$watch('description', this.save);
this.$watch('location', this.save);
this.$watch('birthday', this.save);
this.$watch('lang', this.save);
this.$watch('isBot', this.save);
this.$watch('isCat', this.save);
this.$watch('alwaysMarkNsfw', this.save);
},
methods: {
changeAvatar(e) {
selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
os.api('i/update', {
avatarId: file.id,
});
});
},
changeBanner(e) {
selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
os.api('i/update', {
bannerId: file.id,
});
});
},
async editMetadata() {
const { canceled, result } = await os.form(this.$ts._profile.metadata, {
fieldName0: {
type: 'string',
label: this.$ts._profile.metadataLabel + ' 1',
default: this.fieldName0,
},
fieldValue0: {
type: 'string',
label: this.$ts._profile.metadataContent + ' 1',
default: this.fieldValue0,
},
fieldName1: {
type: 'string',
label: this.$ts._profile.metadataLabel + ' 2',
default: this.fieldName1,
},
fieldValue1: {
type: 'string',
label: this.$ts._profile.metadataContent + ' 2',
default: this.fieldValue1,
},
fieldName2: {
type: 'string',
label: this.$ts._profile.metadataLabel + ' 3',
default: this.fieldName2,
},
fieldValue2: {
type: 'string',
label: this.$ts._profile.metadataContent + ' 3',
default: this.fieldValue2,
},
fieldName3: {
type: 'string',
label: this.$ts._profile.metadataLabel + ' 4',
default: this.fieldName3,
},
fieldValue3: {
type: 'string',
label: this.$ts._profile.metadataContent + ' 4',
default: this.fieldValue3,
},
});
if (canceled) return;
this.fieldName0 = result.fieldName0;
this.fieldValue0 = result.fieldValue0;
this.fieldName1 = result.fieldName1;
this.fieldValue1 = result.fieldValue1;
this.fieldName2 = result.fieldName2;
this.fieldValue2 = result.fieldValue2;
this.fieldName3 = result.fieldName3;
this.fieldValue3 = result.fieldValue3;
const fields = [
{ name: this.fieldName0, value: this.fieldValue0 },
{ name: this.fieldName1, value: this.fieldValue1 },
{ name: this.fieldName2, value: this.fieldValue2 },
{ name: this.fieldName3, value: this.fieldValue3 },
];
os.api('i/update', {
fields,
}).then(i => {
os.success();
}).catch(err => {
os.alert({
type: 'error',
text: err.id
});
});
},
save() {
this.saving = true;
os.apiWithDialog('i/update', {
name: this.name || null,
description: this.description || null,
location: this.location || null,
birthday: this.birthday || null,
lang: this.lang || null,
isBot: !!this.isBot,
isCat: !!this.isCat,
alwaysMarkNsfw: !!this.alwaysMarkNsfw,
}).then(i => {
this.saving = false;
this.$i.avatarId = i.avatarId;
this.$i.avatarUrl = i.avatarUrl;
this.$i.bannerId = i.bannerId;
this.$i.bannerUrl = i.bannerUrl;
}).catch(err => {
this.saving = false;
});
},
}
});
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="_section">
<XNotes ref="notes" class="_content" :pagination="pagination"/>
<XNotes class="_content" :pagination="pagination"/>
</div>
</template>

View File

@@ -5,7 +5,7 @@
<option value="replies">{{ $ts.notesAndReplies }}</option>
<option value="files">{{ $ts.withFiles }}</option>
</MkTab>
<XNotes ref="timeline" :no-gap="true" :pagination="pagination"/>
<XNotes :no-gap="true" :pagination="pagination"/>
</div>
</template>

View File

@@ -1,7 +1,11 @@
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
export const emojilist = require('../emojilist.json') as {
export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
export type UnicodeEmojiDef = {
name: string;
keywords: string[];
char: string;
category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
}[];
category: typeof unicodeEmojiCategories[number];
}
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[];

View File

@@ -252,7 +252,7 @@ export function getNoteMenu(props: {
icon: 'fas fa-exclamation-circle',
text: i18n.locale.reportAbuse,
action: () => {
const u = `${url}/notes/${appearNote.id}`;
const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
os.popup(import('@/components/abuse-report-window.vue'), {
user: appearNote.user,
initialComment: `Note: ${u}\n-----\n`

View File

@@ -5,7 +5,7 @@ import * as Acct from 'misskey-js/built/acct';
import * as os from '@/os';
import { userActions } from '@/store';
import { router } from '@/router';
import { $i } from '@/account';
import { $i, iAmModerator } from '@/account';
export function getUserMenu(user) {
const meId = $i ? $i.id : null;
@@ -175,7 +175,7 @@ export function getUserMenu(user) {
action: reportAbuse
}]);
if ($i && ($i.isAdmin || $i.isModerator)) {
if (iAmModerator) {
menu = menu.concat([null, {
icon: 'fas fa-microphone-slash',
text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,

View File

@@ -14,6 +14,10 @@ if (isTouchSupported) {
}, { passive: true });
window.addEventListener('touchend', () => {
// 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、
// touchendイベントでもtouchstartイベントと同様にtrueにする
isTouchUsing = true;
isScreenTouching = false;
}, { passive: true });
}

View File

@@ -1,4 +1,5 @@
import { inject, onUnmounted, Ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { i18n } from '@/i18n';
import * as os from '@/os';
@@ -16,6 +17,17 @@ export function useLeaveGuard(enabled: Ref<boolean>) {
return canceled;
});
} else {
onBeforeRouteLeave(async (to, from) => {
if (!enabled.value) return true;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.locale.leaveConfirm,
});
return !canceled;
});
}
/*

View File

@@ -61,7 +61,11 @@ export default defineComponent({
otherMenuItemIndicated,
post: os.post,
search,
openAccountMenu,
openAccountMenu:(ev) => {
openAccountMenu({
withExtraOperation: true,
}, ev);
},
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');

View File

@@ -76,7 +76,11 @@ export default defineComponent({
iconOnly,
post: os.post,
search,
openAccountMenu,
openAccountMenu:(ev) => {
openAccountMenu({
withExtraOperation: true,
}, ev);
},
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');

View File

@@ -105,7 +105,11 @@ export default defineComponent({
}, 'closed');
},
openAccountMenu,
openAccountMenu:(ev) => {
openAccountMenu({
withExtraOperation: true,
}, ev);
},
}
});
</script>

View File

@@ -72,7 +72,7 @@ export default defineComponent({
this.props = {};
},
onContextmenu(e) {
onContextmenu(ev: MouseEvent) {
os.contextMenu([{
type: 'label',
text: this.path,
@@ -103,7 +103,7 @@ export default defineComponent({
action: () => {
copyToClipboard(this.url);
}
}], e);
}], ev);
}
}
});

View File

@@ -125,7 +125,11 @@ export default defineComponent({
}, 'closed');
},
openAccountMenu,
openAccountMenu:(ev) => {
openAccountMenu({
withExtraOperation: true,
}, ev);
},
}
});
</script>

View File

@@ -167,15 +167,15 @@ export default defineComponent({
if (window._scroll) window._scroll();
},
onContextmenu(e) {
onContextmenu(ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{
@@ -193,7 +193,7 @@ export default defineComponent({
action: () => {
os.pageWindow(path);
}
}], e);
}], ev);
},
onAiClick(ev) {

View File

@@ -207,8 +207,8 @@ export default defineComponent({
return items;
},
onContextmenu(e) {
os.contextMenu(this.getMenu(), e);
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.getMenu(), ev);
},
goTop() {

View File

@@ -64,15 +64,15 @@ export default defineComponent({
history.back();
},
onContextmenu(e) {
onContextmenu(ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(e.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{
@@ -84,7 +84,7 @@ export default defineComponent({
action: () => {
os.pageWindow(path);
}
}], e);
}], ev);
},
}
});

File diff suppressed because it is too large Load Diff