summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2024-07-14 15:27:52 +0900
committerGitHub <noreply@github.com>2024-07-14 15:27:52 +0900
commit3c032dd5b917c97bf8fb1b87a69f36d56537f493 (patch)
treedacd9e664ed11cc77a351a81d48c52c6f7116c61 /packages
parentenhance(frontend): サーバー情報・お問い合わせページを改修 ... (diff)
downloadmisskey-3c032dd5b917c97bf8fb1b87a69f36d56537f493.tar.gz
misskey-3c032dd5b917c97bf8fb1b87a69f36d56537f493.tar.bz2
misskey-3c032dd5b917c97bf8fb1b87a69f36d56537f493.zip
enhance: 非ログイン時には別サーバーに遷移できるように (#13089)
* enhance: 非ログイン時にはMisskey Hub経由で別サーバーに遷移できるように * fix * サーバーサイド照会を削除 * クライアント側の照会動作 * hubを経由せずにリモートで続行できるように * fix と pleaseLogin誘導箇所の追加 * fix * fix * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue6
-rw-r--r--packages/frontend/src/components/MkNote.vue14
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue14
-rw-r--r--packages/frontend/src/components/MkPoll.vue8
-rw-r--r--packages/frontend/src/components/MkSignin.vue133
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue9
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue2
-rw-r--r--packages/frontend/src/os.ts10
-rw-r--r--packages/frontend/src/pages/flash/flash.vue3
-rw-r--r--packages/frontend/src/pages/follow.vue71
-rw-r--r--packages/frontend/src/pages/lookup.vue97
-rw-r--r--packages/frontend/src/pages/user/home.vue4
-rw-r--r--packages/frontend/src/router/definition.ts12
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts4
-rw-r--r--packages/frontend/src/scripts/please-login.ts16
-rw-r--r--packages/frontend/src/scripts/url.ts5
16 files changed, 297 insertions, 111 deletions
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index ea76950c0d..d8ac8024b4 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -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 {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 420ff2c651..c518c7dd41 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -196,6 +196,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 { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -278,6 +279,11 @@ const renoteCollapsed = ref(
),
);
+const pleaseLoginContext = {
+ type: 'lookup',
+ path: `https://${host}/notes/${appearNote.value.id}`,
+} as const;
+
/* 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';
@@ -411,7 +417,7 @@ if (!props.mock) {
}
function renote(viaKeyboard = false) {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
@@ -421,7 +427,7 @@ function renote(viaKeyboard = false) {
}
function reply(): void {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
if (props.mock) {
return;
}
@@ -434,7 +440,7 @@ function reply(): void {
}
function react(): void {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.playMisskeySfx('reaction');
@@ -565,7 +571,7 @@ function showRenoteMenu(): void {
}
if (isMyRenote) {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
os.popupMenu([
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index a8fed56c39..737e9a853a 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -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 = {
+ type: 'lookup',
+ path: `https://${host}/notes/${appearNote.value.id}`,
+} as const;
+
const keymap = {
'r': () => reply(),
'e|a|plus': () => react(),
@@ -396,7 +402,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
}
function renote() {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
@@ -404,7 +410,7 @@ function renote() {
}
function reply(): void {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
showMovedDialog();
os.post({
reply: appearNote.value,
@@ -415,7 +421,7 @@ function reply(): void {
}
function react(): void {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
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);
os.popupMenu([{
text: i18n.ts.unrenote,
icon: 'ti ti-trash',
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index a98690f1c3..82e2a605f1 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -34,6 +34,7 @@ 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';
const props = defineProps<{
@@ -60,6 +61,11 @@ const timer = computed(() => i18n.tsx._poll[
const showResult = ref(props.readOnly || isVoted.value);
+const pleaseLoginContext = {
+ type: 'lookup',
+ path: `https://${host}/notes/${props.note.id}`,
+} as const;
+
// 期限付きアンケート
if (props.poll.expiresAt) {
const tick = () => {
@@ -76,7 +82,7 @@ if (props.poll.expiresAt) {
}
const vote = async (id) => {
- pleaseLogin();
+ pleaseLogin(undefined, pleaseLoginContext);
if (props.readOnly || closed.value || isVoted.value) return;
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index db32cdd6a1..746ddd7154 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -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,60 @@ function resetPassword(): void {
closed: () => dispose(),
});
}
+
+function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
+ switch (options.type) {
+ case 'web':
+ case 'lookup': {
+ let _path = options.path;
+
+ if (options.type === 'lookup') {
+ // TODO: v2024.2.0以降が浸透してきたら正式なURLに変更する▼
+ // _path = `/lookup?uri=${encodeURIComponent(_path)}`;
+ _path = `/authorize-follow?acct=${encodeURIComponent(_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 +297,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>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 33355bb99e..524c62b4d3 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -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<{
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index f247ba8fdd..d6f1ae453c 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -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>
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 5e332533ef..a0b0e6c833 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -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 { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js';
@@ -670,6 +671,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でテキストエリアに自動フォーカスできない
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 40499fde0e..8a63176d00 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.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',
diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue
deleted file mode 100644
index 247b0ac639..0000000000
--- a/packages/frontend/src/pages/follow.vue
+++ /dev/null
@@ -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>
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
new file mode 100644
index 0000000000..3233953942
--- /dev/null
+++ b/packages/frontend/src/pages/lookup.vue
@@ -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>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 834d799072..d67990e9a2 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -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/>
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 12ab633af1..f7a219c57e 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -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',
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index ac8774fad0..2d1fea8ea4 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -186,7 +186,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: () => {
@@ -259,7 +259,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
},
}));
},
- }] as any;
+ }] : [])] as any;
if ($i && meId !== user.id) {
if (iAmModerator) {
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index 363da5f633..b04062a58a 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -8,12 +8,24 @@ import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { popup } from '@/os.js';
-export function pleaseLogin(path?: string) {
+export type OpenOnRemoteOptions = {
+ type: 'web';
+ path: string;
+} | {
+ type: 'lookup';
+ path: string;
+} | {
+ type: '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) {
diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts
index e3072b3b7d..c477fb5506 100644
--- a/packages/frontend/src/scripts/url.ts
+++ b/packages/frontend/src/scripts/url.ts
@@ -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)?:?\/{0,2}([^\/]+)/);
+ return match ? match[2] : null;
+}