enhance: 非ログイン時には別サーバーに遷移できるように (#13089)
* enhance: 非ログイン時にはMisskey Hub経由で別サーバーに遷移できるように * fix * サーバーサイド照会を削除 * クライアント側の照会動作 * hubを経由せずにリモートで続行できるように * fix と pleaseLogin誘導箇所の追加 * fix * fix * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									6dd6fcf88f
								
							
						
					
					
						commit
						3c032dd5b9
					
				
					 19 changed files with 330 additions and 113 deletions
				
			
		| 
						 | 
					@ -13,6 +13,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Client
 | 
					### Client
 | 
				
			||||||
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
 | 
					- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
 | 
				
			||||||
 | 
					- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
 | 
				
			||||||
- Enhance: 非ログイン時のハイライトTLのデザインを改善
 | 
					- Enhance: 非ログイン時のハイライトTLのデザインを改善
 | 
				
			||||||
- Enhance: フロントエンドのアクセシビリティ改善  
 | 
					- Enhance: フロントエンドのアクセシビリティ改善  
 | 
				
			||||||
  (Based on https://github.com/taiyme/misskey/pull/226)
 | 
					  (Based on https://github.com/taiyme/misskey/pull/226)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										26
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -736,6 +736,22 @@ export interface Locale extends ILocale {
 | 
				
			||||||
     * リモートで表示
 | 
					     * リモートで表示
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "showOnRemote": string;
 | 
					    "showOnRemote": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * リモートで続行
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "continueOnRemote": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Misskey Hubからサーバーを選択
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "chooseServerOnMisskeyHub": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * サーバーのドメインを直接指定
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "specifyServerHost": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * ドメインを入力してください
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "inputHostName": string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 全般
 | 
					     * 全般
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					@ -1921,9 +1937,13 @@ export interface Locale extends ILocale {
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "onlyOneFileCanBeAttached": string;
 | 
					    "onlyOneFileCanBeAttached": string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 続行する前に、サインアップまたはサインインが必要です
 | 
					     * 続行する前に、登録またはログインが必要です
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "signinRequired": string;
 | 
					    "signinRequired": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "signinOrContinueOnRemote": string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 招待
 | 
					     * 招待
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					@ -4984,6 +5004,10 @@ export interface Locale extends ILocale {
 | 
				
			||||||
     * お問い合わせ
 | 
					     * お問い合わせ
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "inquiry": string;
 | 
					    "inquiry": string;
 | 
				
			||||||
 | 
					     /**
 | 
				
			||||||
 | 
					     * もう一度お試しください。
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "tryAgain": string;
 | 
				
			||||||
    "_delivery": {
 | 
					    "_delivery": {
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * 配信状態
 | 
					         * 配信状態
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -180,6 +180,10 @@ addAccount: "アカウントを追加"
 | 
				
			||||||
reloadAccountsList: "アカウントリストの情報を更新"
 | 
					reloadAccountsList: "アカウントリストの情報を更新"
 | 
				
			||||||
loginFailed: "ログインに失敗しました"
 | 
					loginFailed: "ログインに失敗しました"
 | 
				
			||||||
showOnRemote: "リモートで表示"
 | 
					showOnRemote: "リモートで表示"
 | 
				
			||||||
 | 
					continueOnRemote: "リモートで続行"
 | 
				
			||||||
 | 
					chooseServerOnMisskeyHub: "Misskey Hubからサーバーを選択"
 | 
				
			||||||
 | 
					specifyServerHost: "サーバーのドメインを直接指定"
 | 
				
			||||||
 | 
					inputHostName: "ドメインを入力してください"
 | 
				
			||||||
general: "全般"
 | 
					general: "全般"
 | 
				
			||||||
wallpaper: "壁紙"
 | 
					wallpaper: "壁紙"
 | 
				
			||||||
setWallpaper: "壁紙を設定"
 | 
					setWallpaper: "壁紙を設定"
 | 
				
			||||||
| 
						 | 
					@ -476,7 +480,8 @@ attachAsFileQuestion: "クリップボードのテキストが長いです。テ
 | 
				
			||||||
noMessagesYet: "まだチャットはありません"
 | 
					noMessagesYet: "まだチャットはありません"
 | 
				
			||||||
newMessageExists: "新しいメッセージがあります"
 | 
					newMessageExists: "新しいメッセージがあります"
 | 
				
			||||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
 | 
					onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
 | 
				
			||||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
 | 
					signinRequired: "続行する前に、登録またはログインが必要です"
 | 
				
			||||||
 | 
					signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
 | 
				
			||||||
invitations: "招待"
 | 
					invitations: "招待"
 | 
				
			||||||
invitationCode: "招待コード"
 | 
					invitationCode: "招待コード"
 | 
				
			||||||
checking: "確認しています"
 | 
					checking: "確認しています"
 | 
				
			||||||
| 
						 | 
					@ -1242,6 +1247,7 @@ keepOriginalFilenameDescription: "この設定をオフにすると、アップ
 | 
				
			||||||
noDescription: "説明文はありません"
 | 
					noDescription: "説明文はありません"
 | 
				
			||||||
alwaysConfirmFollow: "フォローの際常に確認する"
 | 
					alwaysConfirmFollow: "フォローの際常に確認する"
 | 
				
			||||||
inquiry: "お問い合わせ"
 | 
					inquiry: "お問い合わせ"
 | 
				
			||||||
 | 
					tryAgain: "もう一度お試しください。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_delivery:
 | 
					_delivery:
 | 
				
			||||||
  status: "配信状態"
 | 
					  status: "配信状態"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
import { useStream } from '@/stream.js';
 | 
					import { useStream } from '@/stream.js';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
import { claimAchievement } from '@/scripts/achievements.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 { $i } from '@/account.js';
 | 
				
			||||||
import { defaultStore } from '@/store.js';
 | 
					import { defaultStore } from '@/store.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro
 | 
				
			||||||
const wait = ref(false);
 | 
					const wait = ref(false);
 | 
				
			||||||
const connection = useStream().useChannel('main');
 | 
					const connection = useStream().useChannel('main');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (props.user.isFollowing == null) {
 | 
					if (props.user.isFollowing == null && $i) {
 | 
				
			||||||
	misskeyApi('users/show', {
 | 
						misskeyApi('users/show', {
 | 
				
			||||||
		userId: props.user.id,
 | 
							userId: props.user.id,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
| 
						 | 
					@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function onClick() {
 | 
					async function onClick() {
 | 
				
			||||||
 | 
						pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wait.value = true;
 | 
						wait.value = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -196,6 +196,7 @@ import { MenuItem } from '@/types/menu.js';
 | 
				
			||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
 | 
					import MkRippleEffect from '@/components/MkRippleEffect.vue';
 | 
				
			||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 | 
					import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 | 
				
			||||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
 | 
					import { shouldCollapsed } from '@/scripts/collapsed.js';
 | 
				
			||||||
 | 
					import { host } from '@/config.js';
 | 
				
			||||||
import { isEnabledUrlPreview } from '@/instance.js';
 | 
					import { isEnabledUrlPreview } from '@/instance.js';
 | 
				
			||||||
import { type Keymap } from '@/scripts/hotkey.js';
 | 
					import { type Keymap } from '@/scripts/hotkey.js';
 | 
				
			||||||
import { focusPrev, focusNext } from '@/scripts/focus.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が対応していないのでコメントアウト
 | 
					/* 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: true): boolean;
 | 
				
			||||||
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
 | 
					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) {
 | 
					function renote(viaKeyboard = false) {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	showMovedDialog();
 | 
						showMovedDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
 | 
						const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock });
 | 
				
			||||||
| 
						 | 
					@ -421,7 +427,7 @@ function renote(viaKeyboard = false) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function reply(): void {
 | 
					function reply(): void {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	if (props.mock) {
 | 
						if (props.mock) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -434,7 +440,7 @@ function reply(): void {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function react(): void {
 | 
					function react(): void {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	showMovedDialog();
 | 
						showMovedDialog();
 | 
				
			||||||
	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 | 
						if (appearNote.value.reactionAcceptance === 'likeOnly') {
 | 
				
			||||||
		sound.playMisskeySfx('reaction');
 | 
							sound.playMisskeySfx('reaction');
 | 
				
			||||||
| 
						 | 
					@ -565,7 +571,7 @@ function showRenoteMenu(): void {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (isMyRenote) {
 | 
						if (isMyRenote) {
 | 
				
			||||||
		pleaseLogin();
 | 
							pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
		os.popupMenu([
 | 
							os.popupMenu([
 | 
				
			||||||
			getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 | 
								getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 | 
				
			||||||
			{ type: 'divider' },
 | 
								{ type: 'divider' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -222,6 +222,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js';
 | 
				
			||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 | 
					import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 | 
				
			||||||
import { $i } from '@/account.js';
 | 
					import { $i } from '@/account.js';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
 | 
					import { host } from '@/config.js';
 | 
				
			||||||
import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
 | 
					import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
 | 
				
			||||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
 | 
					import { useNoteCapture } from '@/scripts/use-note-capture.js';
 | 
				
			||||||
import { deepClone } from '@/scripts/clone.js';
 | 
					import { deepClone } from '@/scripts/clone.js';
 | 
				
			||||||
| 
						 | 
					@ -296,6 +297,11 @@ const conversation = ref<Misskey.entities.Note[]>([]);
 | 
				
			||||||
const replies = 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 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 = {
 | 
					const keymap = {
 | 
				
			||||||
	'r': () => reply(),
 | 
						'r': () => reply(),
 | 
				
			||||||
	'e|a|plus': () => react(),
 | 
						'e|a|plus': () => react(),
 | 
				
			||||||
| 
						 | 
					@ -396,7 +402,7 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function renote() {
 | 
					function renote() {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	showMovedDialog();
 | 
						showMovedDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { menu } = getRenoteMenu({ note: note.value, renoteButton });
 | 
						const { menu } = getRenoteMenu({ note: note.value, renoteButton });
 | 
				
			||||||
| 
						 | 
					@ -404,7 +410,7 @@ function renote() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function reply(): void {
 | 
					function reply(): void {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	showMovedDialog();
 | 
						showMovedDialog();
 | 
				
			||||||
	os.post({
 | 
						os.post({
 | 
				
			||||||
		reply: appearNote.value,
 | 
							reply: appearNote.value,
 | 
				
			||||||
| 
						 | 
					@ -415,7 +421,7 @@ function reply(): void {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function react(): void {
 | 
					function react(): void {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	showMovedDialog();
 | 
						showMovedDialog();
 | 
				
			||||||
	if (appearNote.value.reactionAcceptance === 'likeOnly') {
 | 
						if (appearNote.value.reactionAcceptance === 'likeOnly') {
 | 
				
			||||||
		sound.playMisskeySfx('reaction');
 | 
							sound.playMisskeySfx('reaction');
 | 
				
			||||||
| 
						 | 
					@ -499,7 +505,7 @@ async function clip(): Promise<void> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function showRenoteMenu(): void {
 | 
					function showRenoteMenu(): void {
 | 
				
			||||||
	if (!isMyRenote) return;
 | 
						if (!isMyRenote) return;
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		text: i18n.ts.unrenote,
 | 
							text: i18n.ts.unrenote,
 | 
				
			||||||
		icon: 'ti ti-trash',
 | 
							icon: 'ti ti-trash',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
 | 
				
			||||||
import * as os from '@/os.js';
 | 
					import * as os from '@/os.js';
 | 
				
			||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
					import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
 | 
					import { host } from '@/config.js';
 | 
				
			||||||
import { useInterval } from '@/scripts/use-interval.js';
 | 
					import { useInterval } from '@/scripts/use-interval.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
| 
						 | 
					@ -60,6 +61,11 @@ const timer = computed(() => i18n.tsx._poll[
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const showResult = ref(props.readOnly || isVoted.value);
 | 
					const showResult = ref(props.readOnly || isVoted.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const pleaseLoginContext = {
 | 
				
			||||||
 | 
						type: 'lookup',
 | 
				
			||||||
 | 
						path: `https://${host}/notes/${props.note.id}`,
 | 
				
			||||||
 | 
					} as const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 期限付きアンケート
 | 
					// 期限付きアンケート
 | 
				
			||||||
if (props.poll.expiresAt) {
 | 
					if (props.poll.expiresAt) {
 | 
				
			||||||
	const tick = () => {
 | 
						const tick = () => {
 | 
				
			||||||
| 
						 | 
					@ -76,7 +82,7 @@ if (props.poll.expiresAt) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const vote = async (id) => {
 | 
					const vote = async (id) => {
 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin(undefined, pleaseLoginContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (props.readOnly || closed.value || isVoted.value) return;
 | 
						if (props.readOnly || closed.value || isVoted.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,10 +6,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
 | 
					<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
 | 
				
			||||||
	<div class="_gaps_m">
 | 
						<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">
 | 
							<MkInfo v-if="message">
 | 
				
			||||||
			{{ message }}
 | 
								{{ message }}
 | 
				
			||||||
		</MkInfo>
 | 
							</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">
 | 
							<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">
 | 
								<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>
 | 
									<template #prefix>@</template>
 | 
				
			||||||
| 
						 | 
					@ -28,8 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
					{{ i18n.ts.retry }}
 | 
										{{ i18n.ts.retry }}
 | 
				
			||||||
				</MkButton>
 | 
									</MkButton>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div v-if="user && user.securityKeys" class="or-hr">
 | 
								<div v-if="user && user.securityKeys" :class="$style.orHr">
 | 
				
			||||||
				<p class="or-msg">{{ i18n.ts.or }}</p>
 | 
									<p :class="$style.orMsg">{{ i18n.ts.or }}</p>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div class="twofa-group totp-group _gaps">
 | 
								<div class="twofa-group totp-group _gaps">
 | 
				
			||||||
				<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
 | 
									<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 { toUnicode } from 'punycode/';
 | 
				
			||||||
import * as Misskey from 'misskey-js';
 | 
					import * as Misskey from 'misskey-js';
 | 
				
			||||||
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
 | 
					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 { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 | 
				
			||||||
import MkButton from '@/components/MkButton.vue';
 | 
					import MkButton from '@/components/MkButton.vue';
 | 
				
			||||||
import MkInput from '@/components/MkInput.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 { host as configHost } from '@/config.js';
 | 
				
			||||||
import * as os from '@/os.js';
 | 
					import * as os from '@/os.js';
 | 
				
			||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
					import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
				
			||||||
 | 
					import { query, extractDomain } from '@/scripts/url.js';
 | 
				
			||||||
import { login } from '@/account.js';
 | 
					import { login } from '@/account.js';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,22 +93,16 @@ const emit = defineEmits<{
 | 
				
			||||||
	(ev: 'login', v: any): void;
 | 
						(ev: 'login', v: any): void;
 | 
				
			||||||
}>();
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	withAvatar: {
 | 
						withAvatar?: boolean;
 | 
				
			||||||
		type: Boolean,
 | 
						autoSet?: boolean;
 | 
				
			||||||
		required: false,
 | 
						message?: string,
 | 
				
			||||||
		default: true,
 | 
						openOnRemote?: OpenOnRemoteOptions,
 | 
				
			||||||
	},
 | 
					}>(), {
 | 
				
			||||||
	autoSet: {
 | 
						withAvatar: true,
 | 
				
			||||||
		type: Boolean,
 | 
						autoSet: false,
 | 
				
			||||||
		required: false,
 | 
						message: '',
 | 
				
			||||||
		default: false,
 | 
						openOnRemote: undefined,
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	message: {
 | 
					 | 
				
			||||||
		type: String,
 | 
					 | 
				
			||||||
		required: false,
 | 
					 | 
				
			||||||
		default: '',
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function onUsernameChange(): void {
 | 
					function onUsernameChange(): void {
 | 
				
			||||||
| 
						 | 
					@ -222,6 +231,60 @@ function resetPassword(): void {
 | 
				
			||||||
		closed: () => dispose(),
 | 
							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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" module>
 | 
					<style lang="scss" module>
 | 
				
			||||||
| 
						 | 
					@ -234,4 +297,36 @@ function resetPassword(): void {
 | 
				
			||||||
	background-size: cover;
 | 
						background-size: cover;
 | 
				
			||||||
	border-radius: 100%;
 | 
						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>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkModalWindow
 | 
					<MkModalWindow
 | 
				
			||||||
	ref="dialog"
 | 
						ref="dialog"
 | 
				
			||||||
	:width="370"
 | 
						:width="400"
 | 
				
			||||||
	:height="400"
 | 
						:height="430"
 | 
				
			||||||
	@close="onClose"
 | 
						@close="onClose"
 | 
				
			||||||
	@closed="emit('closed')"
 | 
						@closed="emit('closed')"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<template #header>{{ i18n.ts.login }}</template>
 | 
						<template #header>{{ i18n.ts.login }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<MkSpacer :marginMin="20" :marginMax="28">
 | 
						<MkSpacer :marginMin="20" :marginMax="28">
 | 
				
			||||||
		<MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/>
 | 
							<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
 | 
				
			||||||
	</MkSpacer>
 | 
						</MkSpacer>
 | 
				
			||||||
</MkModalWindow>
 | 
					</MkModalWindow>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { shallowRef } from 'vue';
 | 
					import { shallowRef } from 'vue';
 | 
				
			||||||
 | 
					import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 | 
				
			||||||
import MkSignin from '@/components/MkSignin.vue';
 | 
					import MkSignin from '@/components/MkSignin.vue';
 | 
				
			||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
					import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
| 
						 | 
					@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js';
 | 
				
			||||||
withDefaults(defineProps<{
 | 
					withDefaults(defineProps<{
 | 
				
			||||||
	autoSet?: boolean;
 | 
						autoSet?: boolean;
 | 
				
			||||||
	message?: string,
 | 
						message?: string,
 | 
				
			||||||
 | 
						openOnRemote?: OpenOnRemoteOptions,
 | 
				
			||||||
}>(), {
 | 
					}>(), {
 | 
				
			||||||
	autoSet: false,
 | 
						autoSet: false,
 | 
				
			||||||
	message: '',
 | 
						message: '',
 | 
				
			||||||
 | 
						openOnRemote: undefined,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					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>
 | 
								<p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</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>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,6 +23,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue';
 | 
				
			||||||
import MkContextMenu from '@/components/MkContextMenu.vue';
 | 
					import MkContextMenu from '@/components/MkContextMenu.vue';
 | 
				
			||||||
import { MenuItem } from '@/types/menu.js';
 | 
					import { MenuItem } from '@/types/menu.js';
 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.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 { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 | 
				
			||||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
 | 
					import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
 | 
				
			||||||
import { focusParent } from '@/scripts/focus.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> {
 | 
					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();
 | 
						showMovedDialog();
 | 
				
			||||||
	return new Promise(resolve => {
 | 
						return new Promise(resolve => {
 | 
				
			||||||
		// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 | 
							// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,6 +79,7 @@ import { defaultStore } from '@/store.js';
 | 
				
			||||||
import { $i } from '@/account.js';
 | 
					import { $i } from '@/account.js';
 | 
				
			||||||
import { isSupportShare } from '@/scripts/navigator.js';
 | 
					import { isSupportShare } from '@/scripts/navigator.js';
 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 | 
				
			||||||
 | 
					import { pleaseLogin } from '@/scripts/please-login.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	id: string;
 | 
						id: string;
 | 
				
			||||||
| 
						 | 
					@ -143,6 +144,7 @@ function shareWithNote() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function like() {
 | 
					function like() {
 | 
				
			||||||
	if (!flash.value) return;
 | 
						if (!flash.value) return;
 | 
				
			||||||
 | 
						pleaseLogin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	os.apiWithDialog('flash/like', {
 | 
						os.apiWithDialog('flash/like', {
 | 
				
			||||||
		flashId: flash.value.id,
 | 
							flashId: flash.value.id,
 | 
				
			||||||
| 
						 | 
					@ -154,6 +156,7 @@ function like() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function unlike() {
 | 
					async function unlike() {
 | 
				
			||||||
	if (!flash.value) return;
 | 
						if (!flash.value) return;
 | 
				
			||||||
 | 
						pleaseLogin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const confirm = await os.confirm({
 | 
						const confirm = await os.confirm({
 | 
				
			||||||
		type: 'warning',
 | 
							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>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
						<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
 | 
											<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>
 | 
												<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>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<MkAvatar class="avatar" :user="user" indicator/>
 | 
										<MkAvatar class="avatar" :user="user" indicator/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -237,8 +237,18 @@ const routes: RouteDef[] = [{
 | 
				
			||||||
		origin: 'origin',
 | 
							origin: 'origin',
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
 | 
						// Legacy Compatibility	
 | 
				
			||||||
	path: '/authorize-follow',
 | 
						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,
 | 
						loginRequired: true,
 | 
				
			||||||
}, {
 | 
					}, {
 | 
				
			||||||
	path: '/share',
 | 
						path: '/share',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)}`;
 | 
								const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
 | 
				
			||||||
			copyToClipboard(`${url}/${canonical}`);
 | 
								copyToClipboard(`${url}/${canonical}`);
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}, {
 | 
						}, ...($i ? [{
 | 
				
			||||||
		icon: 'ti ti-mail',
 | 
							icon: 'ti ti-mail',
 | 
				
			||||||
		text: i18n.ts.sendMessage,
 | 
							text: i18n.ts.sendMessage,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
| 
						 | 
					@ -259,7 +259,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			}));
 | 
								}));
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}] as any;
 | 
						}] : [])] as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if ($i && meId !== user.id) {
 | 
						if ($i && meId !== user.id) {
 | 
				
			||||||
		if (iAmModerator) {
 | 
							if (iAmModerator) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,12 +8,24 @@ import { $i } from '@/account.js';
 | 
				
			||||||
import { i18n } from '@/i18n.js';
 | 
					import { i18n } from '@/i18n.js';
 | 
				
			||||||
import { popup } from '@/os.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;
 | 
						if ($i) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
 | 
						const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
 | 
				
			||||||
		autoSet: true,
 | 
							autoSet: true,
 | 
				
			||||||
		message: i18n.ts.signinRequired,
 | 
							message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired,
 | 
				
			||||||
 | 
							openOnRemote,
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		cancelled: () => {
 | 
							cancelled: () => {
 | 
				
			||||||
			if (path) {
 | 
								if (path) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,3 +21,8 @@ export function query(obj: Record<string, any>): string {
 | 
				
			||||||
export function appendQuery(url: string, query: string): string {
 | 
					export function appendQuery(url: string, query: string): string {
 | 
				
			||||||
	return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
 | 
						return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function extractDomain(url: string) {
 | 
				
			||||||
 | 
						const match = url.match(/^(https)?:?\/{0,2}([^\/]+)/);
 | 
				
			||||||
 | 
						return match ? match[2] : null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue