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
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<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>
|
|
|
@ -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…
Reference in New Issue