Merge branch 'develop' into feat-1714
This commit is contained in:
commit
8d144c8fdc
85 changed files with 1222 additions and 831 deletions
|
|
@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { host } from '@/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
|
|||
const wait = ref(false);
|
||||
const connection = useStream().useChannel('main');
|
||||
|
||||
if (props.user.isFollowing == null) {
|
||||
if (props.user.isFollowing == null && $i) {
|
||||
misskeyApi('users/show', {
|
||||
userId: props.user.id,
|
||||
})
|
||||
|
|
@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
|
|||
}
|
||||
|
||||
async function onClick() {
|
||||
pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
|
||||
|
||||
wait.value = true;
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:class="['_button', $style.item]"
|
||||
:href="item.href"
|
||||
:target="item.target"
|
||||
:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
|
||||
:download="item.download"
|
||||
@click.passive="close(true)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
|||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
|
|
@ -214,6 +214,7 @@ import { MenuItem } from '@/types/menu.js';
|
|||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { host } from '@/config.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { url } from '@/config.js';
|
||||
import { type Keymap } from '@/scripts/hotkey.js';
|
||||
|
|
@ -298,6 +299,11 @@ const renoteCollapsed = ref(
|
|||
),
|
||||
);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
}));
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
|
||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
|
||||
|
|
@ -431,7 +437,7 @@ if (!props.mock && !inEmbedPage) {
|
|||
}
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
|
||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
|
||||
|
|
@ -441,7 +447,7 @@ function renote(viaKeyboard = false) {
|
|||
}
|
||||
|
||||
function reply(): void {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -454,7 +460,7 @@ function reply(): void {
|
|||
}
|
||||
|
||||
function react(): void {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
|
@ -585,7 +591,7 @@ function showRenoteMenu(): void {
|
|||
}
|
||||
|
||||
if (isMyRenote) {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||
{ type: 'divider' },
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
|||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
|
|
@ -222,6 +222,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
|
|||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
|
|
@ -296,6 +297,11 @@ const conversation = ref<Misskey.entities.Note[]>([]);
|
|||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${appearNote.value.id}`,
|
||||
}));
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(),
|
||||
'e|a|plus': () => react(),
|
||||
|
|
@ -396,7 +402,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
|||
}
|
||||
|
||||
function renote() {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
|
||||
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
|
||||
|
|
@ -404,7 +410,7 @@ function renote() {
|
|||
}
|
||||
|
||||
function reply(): void {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
os.post({
|
||||
reply: appearNote.value,
|
||||
|
|
@ -415,7 +421,7 @@ function reply(): void {
|
|||
}
|
||||
|
||||
function react(): void {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
showMovedDialog();
|
||||
if (appearNote.value.reactionAcceptance === 'likeOnly') {
|
||||
sound.playMisskeySfx('reaction');
|
||||
|
|
@ -499,7 +505,7 @@ async function clip(): Promise<void> {
|
|||
|
||||
function showRenoteMenu(): void {
|
||||
if (!isMyRenote) return;
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host } from '@/config.js';
|
||||
import { useInterval } from '@/scripts/use-interval.js';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: string;
|
||||
|
|
@ -60,6 +62,11 @@ const timer = computed(() => i18n.tsx._poll[
|
|||
|
||||
const showResult = ref(props.readOnly || isVoted.value);
|
||||
|
||||
const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
|
||||
type: 'lookup',
|
||||
url: `https://${host}/notes/${props.noteId}`,
|
||||
}));
|
||||
|
||||
// 期限付きアンケート
|
||||
if (props.poll.expiresAt) {
|
||||
const tick = () => {
|
||||
|
|
@ -76,7 +83,7 @@ if (props.poll.expiresAt) {
|
|||
}
|
||||
|
||||
const vote = async (id) => {
|
||||
pleaseLogin();
|
||||
pleaseLogin(undefined, pleaseLoginContext.value);
|
||||
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="_gaps_m">
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="openOnRemote" class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
|
||||
{{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
|
||||
</MkButton>
|
||||
<button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
|
||||
{{ i18n.ts.specifyServerHost }}
|
||||
</button>
|
||||
</div>
|
||||
<div :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
|
|
@ -28,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
{{ i18n.ts.retry }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="user && user.securityKeys" class="or-hr">
|
||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
||||
<div v-if="user && user.securityKeys" :class="$style.orHr">
|
||||
<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group _gaps">
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
|
|
@ -53,6 +66,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
|||
import { toUnicode } from 'punycode/';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -60,6 +74,7 @@ import MkInfo from '@/components/MkInfo.vue';
|
|||
import { host as configHost } from '@/config.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { query, extractDomain } from '@/scripts/url.js';
|
||||
import { login } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
|
@ -78,22 +93,16 @@ const emit = defineEmits<{
|
|||
(ev: 'login', v: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
withAvatar?: boolean;
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
openOnRemote?: OpenOnRemoteOptions,
|
||||
}>(), {
|
||||
withAvatar: true,
|
||||
autoSet: false,
|
||||
message: '',
|
||||
openOnRemote: undefined,
|
||||
});
|
||||
|
||||
function onUsernameChange(): void {
|
||||
|
|
@ -222,6 +231,62 @@ function resetPassword(): void {
|
|||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
|
||||
switch (options.type) {
|
||||
case 'web':
|
||||
case 'lookup': {
|
||||
let _path: string;
|
||||
|
||||
if (options.type === 'lookup') {
|
||||
// TODO: v2024.7.0以降が浸透してきたら正式なURLに変更する▼
|
||||
// _path = `/lookup?uri=${encodeURIComponent(_path)}`;
|
||||
_path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
|
||||
} else {
|
||||
_path = options.path;
|
||||
}
|
||||
|
||||
if (targetHost) {
|
||||
window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
|
||||
} else {
|
||||
window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'share': {
|
||||
const params = query(options.params);
|
||||
if (targetHost) {
|
||||
window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
|
||||
} else {
|
||||
window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
|
||||
const { canceled, result: hostTemp } = await os.inputText({
|
||||
title: i18n.ts.inputHostName,
|
||||
placeholder: 'misskey.example.com',
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
let targetHost: string | null = hostTemp;
|
||||
|
||||
// ドメイン部分だけを取り出す
|
||||
targetHost = extractDomain(targetHost);
|
||||
if (targetHost == null) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.invalidValue,
|
||||
text: i18n.ts.tryAgain,
|
||||
});
|
||||
return;
|
||||
}
|
||||
openRemote(options, targetHost);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -234,4 +299,36 @@ function resetPassword(): void {
|
|||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.instanceManualSelectButton {
|
||||
display: block;
|
||||
text-align: center;
|
||||
opacity: .7;
|
||||
font-size: .8em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.orHr {
|
||||
position: relative;
|
||||
margin: .4em auto;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--divider);
|
||||
}
|
||||
|
||||
.orMsg {
|
||||
position: absolute;
|
||||
top: -.6em;
|
||||
display: inline-block;
|
||||
padding: 0 1em;
|
||||
background: var(--panel);
|
||||
font-size: 0.8em;
|
||||
color: var(--fgOnPanel);
|
||||
margin: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
:width="400"
|
||||
:height="430"
|
||||
@close="onClose"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
|
||||
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
|
|||
withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
openOnRemote?: OpenOnRemoteOptions,
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
message: '',
|
||||
openOnRemote: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
|
||||
<MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div class="_gaps_s" :class="$style.mainActions">
|
||||
<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
|
||||
<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
|
|||
import { instance } from '@/instance.js';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
|
||||
import { openInstanceMenu } from '@/ui/_common_/common';
|
||||
import { openInstanceMenu } from '@/ui/_common_/common.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
|
||||
|
|
@ -89,13 +90,9 @@ function signup() {
|
|||
});
|
||||
}
|
||||
|
||||
function showMenu(ev) {
|
||||
function showMenu(ev: MouseEvent) {
|
||||
openInstanceMenu(ev);
|
||||
}
|
||||
|
||||
function exploreOtherServers() {
|
||||
window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
const validTime = (t: string | boolean | null | undefined) => {
|
||||
if (t == null) return null;
|
||||
if (typeof t === 'boolean') return null;
|
||||
return t.match(/^[0-9.]+s$/) ? t : null;
|
||||
return t.match(/^\-?[0-9.]+s$/) ? t : null;
|
||||
};
|
||||
|
||||
const validColor = (c: unknown): string | null => {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function onChange({ resolved, key: newKey }) {
|
|||
if (current == null || 'redirect' in current.route) return;
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
|
||||
key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
nextTick(() => {
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export const ROLE_POLICIES = [
|
|||
'canHideAds',
|
||||
'driveCapacityMb',
|
||||
'alwaysMarkNsfw',
|
||||
'canUpdateBioMedia',
|
||||
'pinLimit',
|
||||
'antennaLimit',
|
||||
'wordMuteLimit',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
|||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { embedPage } from '@/config.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
|
|
@ -684,6 +685,15 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
|||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
pleaseLogin(undefined, (props.initialText || props.initialNote ? {
|
||||
type: 'share',
|
||||
params: {
|
||||
text: props.initialText ?? props.initialNote.text,
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||
},
|
||||
} : undefined));
|
||||
|
||||
showMovedDialog();
|
||||
return new Promise(resolve => {
|
||||
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
|
||||
|
|
|
|||
205
packages/frontend/src/pages/about.overview.vue
Normal file
205
packages/frontend/src/pages/about.overview.vue
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutMisskey }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
</FormLink>
|
||||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue :copy="instance.maintainerName">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>
|
||||
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="instance.maintainerEmail">
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>
|
||||
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<template #value>
|
||||
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<div class="_gaps_s">
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
|
||||
<template #icon><i class="ti ti-user-shield"></i></template>
|
||||
<template #default>{{ i18n.ts.impressum }}</template>
|
||||
</FormLink>
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #icon><i class="ti ti-checkup-list"></i></template>
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
|
||||
<div :class="$style.ruleText" v-html="item"></div>
|
||||
</li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
<template #default>{{ i18n.ts.termsOfService }}</template>
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
|
||||
<template #icon><i class="ti ti-shield-lock"></i></template>
|
||||
<template #default>{{ i18n.ts.privacyPolicy }}</template>
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
|
||||
<template #icon><i class="ti ti-message"></i></template>
|
||||
<template #default>{{ i18n.ts.feedback }}</template>
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense v-slot="{ result: stats }" :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
|
||||
<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
|
||||
<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
|
||||
<FormLink to="/robots.txt" external>robots.txt</FormLink>
|
||||
<FormLink to="/manifest.json" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { host, version } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
const initStats = () => misskeyApi('stats', {});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
background-color: var(--panel);
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
display: block;
|
||||
margin: 16px auto 0 auto;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bannerName {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
.rules {
|
||||
counter-reset: item;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
word-break: break-word;
|
||||
|
||||
&::before {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 8px);
|
||||
counter-increment: item;
|
||||
content: counter(item);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
|
||||
<div style="overflow: clip;">
|
||||
<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
|
||||
<div :class="$style.bannerName">
|
||||
<b>{{ instance.name ?? host }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">
|
||||
<template #icon><i class="ti ti-info-circle"></i></template>
|
||||
{{ i18n.ts.aboutMisskey }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
|
||||
<template #icon><i class="ti ti-code"></i></template>
|
||||
{{ i18n.ts.sourceCode }}
|
||||
</FormLink>
|
||||
<MkInfo v-else warn>
|
||||
{{ i18n.ts.sourceCodeIsNotYetProvided }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
|
||||
<template #icon><i class="ti ti-user-shield"></i></template>
|
||||
{{ i18n.ts.impressum }}
|
||||
</FormLink>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-if="instance.serverRules.length > 0">
|
||||
<template #label>
|
||||
<i class="ti ti-checkup-list"></i>
|
||||
{{ i18n.ts.serverRules }}
|
||||
</template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
</MkFolder>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
|
||||
<template #icon><i class="ti ti-license"></i></template>
|
||||
{{ i18n.ts.termsOfService }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
|
||||
<template #icon><i class="ti ti-shield-lock"></i></template>
|
||||
{{ i18n.ts.privacyPolicy }}
|
||||
</FormLink>
|
||||
<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
|
||||
<template #icon><i class="ti ti-message"></i></template>
|
||||
{{ i18n.ts.feedback }}
|
||||
</FormLink>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
</FormSuspense>
|
||||
|
||||
<FormSection>
|
||||
<template #label>Well-known resources</template>
|
||||
<div class="_gaps_s">
|
||||
<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
|
||||
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
|
||||
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
|
||||
<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
|
||||
<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
<XOverview/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
|
|
@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
import XFederation from './about.federation.vue';
|
||||
import { version, host } from '@/config.js';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkInstanceStats from '@/components/MkInstanceStats.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
|
||||
const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
|
||||
const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
|
||||
const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
|
||||
const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: string;
|
||||
|
|
@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
|
|||
initialTab: 'overview',
|
||||
});
|
||||
|
||||
const stats = ref<Misskey.entities.StatsResponse | null>(null);
|
||||
const tab = ref(props.initialTab);
|
||||
|
||||
watch(tab, () => {
|
||||
|
|
@ -166,11 +49,6 @@ watch(tab, () => {
|
|||
}
|
||||
});
|
||||
|
||||
const initStats = () => misskeyApi('stats', {
|
||||
}).then((res) => {
|
||||
stats.value = res;
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
|
|
@ -195,64 +73,3 @@ definePageMetadata(() => ({
|
|||
icon: 'ti ti-info-circle',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
overflow: clip;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
display: block;
|
||||
margin: 16px auto 0 auto;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bannerName {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
}
|
||||
|
||||
.rules {
|
||||
counter-reset: item;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
word-break: break-word;
|
||||
|
||||
&::before {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 8px);
|
||||
counter-increment: item;
|
||||
content: counter(item);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canUpdateBioMedia.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])">
|
||||
<template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template>
|
||||
<template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canUpdateBioMedia">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||
<template #suffix>{{ policies.pinLimit }}</template>
|
||||
|
|
|
|||
|
|
@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="600" :marginMin="20">
|
||||
<div class="_gaps">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="instance.maintainerName">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>
|
||||
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.email }}</template>
|
||||
<MkKeyValue :copy="instance.maintainerEmail">
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>
|
||||
<div>{{ instance.maintainerEmail }}</div>
|
||||
<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue :copy="instance.inquiryUrl">
|
||||
<template #key>{{ i18n.ts.inquiry }}</template>
|
||||
<template #value>
|
||||
<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
|
||||
<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
|
|
@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ import { defaultStore } from '@/store.js';
|
|||
import { $i } from '@/account.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
|
||||
const props = defineProps<{
|
||||
id: string;
|
||||
|
|
@ -143,6 +144,7 @@ function shareWithNote() {
|
|||
|
||||
function like() {
|
||||
if (!flash.value) return;
|
||||
pleaseLogin();
|
||||
|
||||
os.apiWithDialog('flash/like', {
|
||||
flashId: flash.value.id,
|
||||
|
|
@ -154,6 +156,7 @@ function like() {
|
|||
|
||||
async function unlike() {
|
||||
if (!flash.value) return;
|
||||
pleaseLogin();
|
||||
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
|
||||
async function follow(user): Promise<void> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'question',
|
||||
text: i18n.tsx.followConfirm({ name: user.name || user.username }),
|
||||
});
|
||||
|
||||
if (canceled) {
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
os.apiWithDialog('following/create', {
|
||||
userId: user.id,
|
||||
withReplies: defaultStore.state.defaultWithReplies,
|
||||
});
|
||||
user.withReplies = defaultStore.state.defaultWithReplies;
|
||||
}
|
||||
|
||||
const acct = new URL(location.href).searchParams.get('acct');
|
||||
if (acct == null) {
|
||||
throw new Error('acct required');
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (acct.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
uri: acct,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
follow(res.object);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.push(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
}).then(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
promise = misskeyApi('users/show', Misskey.acct.parse(acct));
|
||||
promise.then(user => {
|
||||
follow(user);
|
||||
});
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
</script>
|
||||
97
packages/frontend/src/pages/lookup.vue
Normal file
97
packages/frontend/src/pages/lookup.vue
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div v-if="state === 'done'" class="_buttonsCenter">
|
||||
<MkButton @click="close">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton>
|
||||
</div>
|
||||
<div v-else class="_fullInfo">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const state = ref<'fetching' | 'done'>('fetching');
|
||||
|
||||
function fetch() {
|
||||
const params = new URL(location.href).searchParams;
|
||||
|
||||
// acctのほうはdeprecated
|
||||
let uri = params.get('uri') ?? params.get('acct');
|
||||
if (uri == null) {
|
||||
state.value = 'done';
|
||||
return;
|
||||
}
|
||||
|
||||
let promise: Promise<any>;
|
||||
|
||||
if (uri.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
uri,
|
||||
});
|
||||
promise.then(res => {
|
||||
if (res.type === 'User') {
|
||||
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
|
||||
} else if (res.type === 'Note') {
|
||||
mainRouter.replace(`/notes/${res.object.id}`);
|
||||
} else {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Not a user',
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (uri.startsWith('acct:')) {
|
||||
uri = uri.slice(5);
|
||||
}
|
||||
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
|
||||
promise.then(user => {
|
||||
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
|
||||
});
|
||||
}
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
window.close();
|
||||
|
||||
// 閉じなければ100ms後タイムラインに
|
||||
window.setTimeout(() => {
|
||||
location.href = '/';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function goToMisskey(): void {
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
fetch();
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.lookup,
|
||||
icon: 'ti ti-world-search',
|
||||
});
|
||||
</script>
|
||||
|
|
@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
|
||||
<div v-if="$i" class="actions">
|
||||
<div class="actions">
|
||||
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
|
||||
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
<MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
|
|
|
|||
|
|
@ -237,8 +237,18 @@ const routes: RouteDef[] = [{
|
|||
origin: 'origin',
|
||||
},
|
||||
}, {
|
||||
// Legacy Compatibility
|
||||
path: '/authorize-follow',
|
||||
component: page(() => import('@/pages/follow.vue')),
|
||||
redirect: '/lookup',
|
||||
loginRequired: true,
|
||||
}, {
|
||||
// Mastodon Compatibility
|
||||
path: '/authorize_interaction',
|
||||
redirect: '/lookup',
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/lookup',
|
||||
component: page(() => import('@/pages/lookup.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/share',
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
|
||||
copyToClipboard(`${url}/${canonical}`);
|
||||
},
|
||||
}, {
|
||||
}, ...($i ? [{
|
||||
icon: 'ti ti-mail',
|
||||
text: i18n.ts.sendMessage,
|
||||
action: () => {
|
||||
|
|
@ -270,7 +270,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
},
|
||||
}));
|
||||
},
|
||||
}] as any;
|
||||
}] : [])] as any;
|
||||
|
||||
if ($i && meId !== user.id) {
|
||||
if (iAmModerator) {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ const matchPatterns = (ev: KeyboardEvent, action: Action) => {
|
|||
const key = ev.key.toLowerCase();
|
||||
return patterns.some(({ which, ctrl, shift, alt }) => {
|
||||
if (
|
||||
options.allowRepeat === false &&
|
||||
latestHotkey != null &&
|
||||
latestHotkey.which.includes(key) &&
|
||||
latestHotkey.ctrl === ctrl &&
|
||||
|
|
|
|||
|
|
@ -8,12 +8,49 @@ import { $i } from '@/account.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export function pleaseLogin(path?: string) {
|
||||
export type OpenOnRemoteOptions = {
|
||||
/**
|
||||
* 外部のMisskey Webで特定のパスを開く
|
||||
*/
|
||||
type: 'web';
|
||||
|
||||
/**
|
||||
* 内部パス(例: `/settings`)
|
||||
*/
|
||||
path: string;
|
||||
} | {
|
||||
/**
|
||||
* 外部のMisskey Webで照会する
|
||||
*/
|
||||
type: 'lookup';
|
||||
|
||||
/**
|
||||
* 照会したいエンティティのURL
|
||||
*
|
||||
* (例: `https://misskey.example.com/notes/abcdexxxxyz`)
|
||||
*/
|
||||
url: string;
|
||||
} | {
|
||||
/**
|
||||
* 外部のMisskeyでノートする
|
||||
*/
|
||||
type: 'share';
|
||||
|
||||
/**
|
||||
* `/share` ページに渡すクエリストリング
|
||||
*
|
||||
* @see https://go.misskey-hub.net/spec/share/
|
||||
*/
|
||||
params: Record<string, string>;
|
||||
};
|
||||
|
||||
export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) {
|
||||
if ($i) return;
|
||||
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
|
||||
autoSet: true,
|
||||
message: i18n.ts.signinRequired,
|
||||
message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
|
||||
openOnRemote,
|
||||
}, {
|
||||
cancelled: () => {
|
||||
if (path) {
|
||||
|
|
|
|||
|
|
@ -21,3 +21,8 @@ export function query(obj: Record<string, any>): string {
|
|||
export function appendQuery(url: string, query: string): string {
|
||||
return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
|
||||
}
|
||||
|
||||
export function extractDomain(url: string) {
|
||||
const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,29 +85,29 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
icon: 'ti ti-help-circle',
|
||||
to: '/contact',
|
||||
}, (instance.impressumUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.impressum,
|
||||
icon: 'ti ti-file-invoice',
|
||||
action: () => {
|
||||
window.open(instance.impressumUrl, '_blank', 'noopener');
|
||||
},
|
||||
href: instance.impressumUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (instance.tosUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.termsOfService,
|
||||
icon: 'ti ti-notebook',
|
||||
action: () => {
|
||||
window.open(instance.tosUrl, '_blank', 'noopener');
|
||||
},
|
||||
href: instance.tosUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (instance.privacyPolicyUrl) ? {
|
||||
type: 'a',
|
||||
text: i18n.ts.privacyPolicy,
|
||||
icon: 'ti ti-shield-lock',
|
||||
action: () => {
|
||||
window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
|
||||
},
|
||||
href: instance.privacyPolicyUrl,
|
||||
target: '_blank',
|
||||
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
|
||||
type: 'a',
|
||||
text: i18n.ts.document,
|
||||
icon: 'ti ti-bulb',
|
||||
action: () => {
|
||||
window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
|
||||
},
|
||||
href: 'https://misskey-hub.net/docs/for-users/',
|
||||
target: '_blank',
|
||||
}, ($i) ? {
|
||||
text: i18n.ts._initialTutorial.launchTutorial,
|
||||
icon: 'ti ti-presentation',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue