Merge branch 'develop' into feat-12997
This commit is contained in:
commit
155692b5ef
139 changed files with 2308 additions and 447 deletions
|
|
@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { bundledLanguagesInfo } from 'shiki';
|
||||
import type { BuiltinLanguage } from 'shiki';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { bundledLanguagesInfo } from 'shiki/langs';
|
||||
import type { BundledLanguage } from 'shiki/langs';
|
||||
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ const props = defineProps<{
|
|||
|
||||
const highlighter = await getHighlighter();
|
||||
const darkMode = defaultStore.reactiveState.darkMode;
|
||||
const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
|
||||
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
|
||||
|
||||
const [lightThemeName, darkThemeName] = await Promise.all([
|
||||
getTheme('light', true),
|
||||
|
|
@ -42,7 +42,7 @@ const html = computed(() => highlighter.codeToHtml(props.code, {
|
|||
}));
|
||||
|
||||
async function fetchLanguage(to: string): Promise<void> {
|
||||
const language = to as BuiltinLanguage;
|
||||
const language = to as BundledLanguage;
|
||||
|
||||
// Check for the loaded languages, and load the language if it's not loaded yet.
|
||||
if (!highlighter.getLoadedLanguages().includes(language)) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:autocomplete="autocomplete"
|
||||
:autocapitalize="autocapitalize"
|
||||
:spellcheck="spellcheck"
|
||||
:inputmode="inputmode"
|
||||
:step="step"
|
||||
:list="id"
|
||||
:min="min"
|
||||
|
|
@ -63,6 +64,7 @@ const props = defineProps<{
|
|||
mfmAutocomplete?: boolean | SuggestionType[],
|
||||
autocapitalize?: string;
|
||||
spellcheck?: boolean;
|
||||
inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
|
||||
step?: any;
|
||||
datalist?: string[];
|
||||
min?: number;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { defineAsyncComponent, ref } from 'vue';
|
|||
import { url as local } from '@/config.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import * as os from '@/os.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
|
|
@ -29,15 +30,17 @@ const self = props.url.startsWith(local);
|
|||
const attr = self ? 'to' : 'href';
|
||||
const target = self ? null : '_blank';
|
||||
|
||||
const el = ref<HTMLElement>();
|
||||
const el = ref<HTMLElement | { $el: HTMLElement }>();
|
||||
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
if (isEnabledUrlPreview.value) {
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
|
|
@ -93,7 +95,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
<template #more>
|
||||
<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
|
||||
</template>
|
||||
|
|
@ -101,7 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<footer :class="$style.footer">
|
||||
<button :class="$style.footerButton" class="_button" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
|
|
@ -111,17 +113,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
</button>
|
||||
<button v-else :class="$style.footerButton" class="_button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
|
|
@ -175,6 +177,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import number from '@/filters/number.js';
|
||||
import * as os from '@/os.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
|
@ -193,6 +196,7 @@ import { MenuItem } from '@/types/menu.js';
|
|||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -267,7 +271,7 @@ const renoteCollapsed = ref(
|
|||
defaultStore.state.collapseRenotes && isRenote && (
|
||||
($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
|
||||
(appearNote.value.myReaction != null)
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||
|
|
@ -420,6 +424,14 @@ function undoReact(targetNote: Misskey.entities.Note): void {
|
|||
});
|
||||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
if (props.mock) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkMediaList :mediaList="appearNote.files"/>
|
||||
</div>
|
||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
|
|
@ -106,10 +108,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
|
||||
</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
|
||||
<button class="_button" :class="$style.noteFooterButton" @click="reply()">
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
|
||||
<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
|
||||
</button>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
|
|
@ -119,17 +121,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
|
||||
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
|
||||
</button>
|
||||
<button v-else class="_button" :class="$style.noteFooterButton" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||
<i v-else class="ti ti-plus"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
|
||||
<i class="ti ti-minus"></i>
|
||||
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
|
||||
</button>
|
||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
||||
<i class="ti ti-paperclip"></i>
|
||||
|
|
@ -209,6 +211,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { checkWordMute } from '@/scripts/check-word-mute.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import number from '@/filters/number.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
|
|
@ -228,6 +231,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|||
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
|
|
@ -401,14 +405,22 @@ function react(viaKeyboard = false): void {
|
|||
}
|
||||
}
|
||||
|
||||
function undoReact(note): void {
|
||||
const oldReaction = note.myReaction;
|
||||
function undoReact(targetNote: Misskey.entities.Note): void {
|
||||
const oldReaction = targetNote.myReaction;
|
||||
if (!oldReaction) return;
|
||||
misskeyApi('notes/reactions/delete', {
|
||||
noteId: note.id,
|
||||
noteId: targetNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleReact() {
|
||||
if (appearNote.value.myReaction == null) {
|
||||
react();
|
||||
} else {
|
||||
undoReact(appearNote.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
const isLink = (el: HTMLElement): boolean => {
|
||||
if (el.tagName === 'A') return true;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.head">
|
||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||
|
|
@ -57,6 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
||||
|
|
@ -201,6 +203,7 @@ const rejectFollowRequest = () => {
|
|||
}
|
||||
|
||||
.icon_reactionGroup,
|
||||
.icon_reactionGroupHeart,
|
||||
.icon_renoteGroup {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
|
@ -213,11 +216,15 @@ const rejectFollowRequest = () => {
|
|||
}
|
||||
|
||||
.icon_reactionGroup {
|
||||
background: #e99a0b;
|
||||
background: var(--eventReaction);
|
||||
}
|
||||
|
||||
.icon_reactionGroupHeart {
|
||||
background: var(--eventReactionHeart);
|
||||
}
|
||||
|
||||
.icon_renoteGroup {
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
}
|
||||
|
||||
.icon_app {
|
||||
|
|
@ -246,49 +253,49 @@ const rejectFollowRequest = () => {
|
|||
|
||||
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
|
||||
padding: 3px;
|
||||
background: #36aed2;
|
||||
background: var(--eventFollow);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_renote {
|
||||
padding: 3px;
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_quote {
|
||||
padding: 3px;
|
||||
background: #36d298;
|
||||
background: var(--eventRenote);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_reply {
|
||||
padding: 3px;
|
||||
background: #007aff;
|
||||
background: var(--eventReply);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_mention {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_pollEnded {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_achievementEarned {
|
||||
padding: 3px;
|
||||
background: #cb9a11;
|
||||
background: var(--eventAchievement);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.t_roleAssigned {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
background: var(--eventOther);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,18 +19,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div style="margin-top: 16px;">{{ i18n.ts.authenticationRequiredToContinue }}</div>
|
||||
</div>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true">
|
||||
<template #prefix><i class="ti ti-password"></i></template>
|
||||
</MkInput>
|
||||
<form @submit.prevent="done">
|
||||
<div class="_gaps">
|
||||
<MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" required :withPasswordToggle="true">
|
||||
<template #prefix><i class="ti ti-password"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false">
|
||||
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
|
||||
<template #prefix><i class="ti ti-123"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-if="$i.twoFactorEnabled" v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
|
||||
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
|
||||
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
|
||||
</div>
|
||||
<MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton>
|
||||
</div>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
|
@ -54,6 +57,7 @@ const emit = defineEmits<{
|
|||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const passwordInput = shallowRef<InstanceType<typeof MkInput>>();
|
||||
const password = ref('');
|
||||
const isBackupCode = ref(false);
|
||||
const token = ref<string | null>(null);
|
||||
|
||||
function onClose() {
|
||||
|
|
@ -61,7 +65,7 @@ function onClose() {
|
|||
if (dialog.value) dialog.value.close();
|
||||
}
|
||||
|
||||
function done(res) {
|
||||
function done() {
|
||||
emit('done', { password: password.value, token: token.value });
|
||||
if (dialog.value) dialog.value.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="user && user.securityKeys" class="or-hr">
|
||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ i18n.ts['2fa'] }}</p>
|
||||
<div class="twofa-group totp-group _gaps">
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="token" type="text" pattern="^([0-9]{6}|[A-Z0-9]{32})$" autocomplete="one-time-code" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.token }}</template>
|
||||
<template #prefix><i class="ti ti-123"></i></template>
|
||||
<MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
|
||||
<template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
|
||||
<template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
|
||||
</MkInput>
|
||||
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
|
|
@ -70,6 +70,7 @@ const password = ref('');
|
|||
const token = ref('');
|
||||
const host = ref(toUnicode(configHost));
|
||||
const totpLogin = ref(false);
|
||||
const isBackupCode = ref(false);
|
||||
const queryingKey = ref(false);
|
||||
const credentialRequest = ref<CredentialRequestOptions | null>(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: [],
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: [],
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||
reactionAcceptance: null,
|
||||
renoteCount: 0,
|
||||
repliesCount: 1,
|
||||
reactionCount: 0,
|
||||
reactions: {},
|
||||
reactionEmojis: {},
|
||||
fileIds: ['0000000002'],
|
||||
|
|
|
|||
|
|
@ -152,15 +152,16 @@ requestUrl.hash = '';
|
|||
window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`)
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
fetching.value = false;
|
||||
unknownUrl.value = true;
|
||||
return;
|
||||
if (_DEV_) {
|
||||
console.warn(`[HTTP${res.status}] Failed to fetch url preview`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((info: SummalyResult) => {
|
||||
if (info.url == null) {
|
||||
.then((info: SummalyResult | null) => {
|
||||
if (!info || info.url == null) {
|
||||
fetching.value = false;
|
||||
unknownUrl.value = true;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<render/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
|
|
@ -26,6 +26,10 @@ const props = withDefaults(defineProps<{
|
|||
behavior: null,
|
||||
});
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
|
||||
defineExpose({ $el: el });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const active = computed(() => {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.form_vertical]: chosen.place === 'vertical',
|
||||
}]"
|
||||
>
|
||||
<a :href="chosen.url" target="_blank" :class="$style.link">
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'"
|
||||
:class="$style.link"
|
||||
v-bind="self ? {
|
||||
to: chosen.url.substring(local.length),
|
||||
} : {
|
||||
href: chosen.url,
|
||||
rel: 'nofollow noopener',
|
||||
target: '_blank',
|
||||
}"
|
||||
>
|
||||
<img :src="chosen.imageUrl" :class="$style.img">
|
||||
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
|
||||
</a>
|
||||
</component>
|
||||
</div>
|
||||
<div v-else :class="$style.menu">
|
||||
<div :class="$style.menuContainer">
|
||||
|
|
@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
import { url as local, host } from '@/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import * as os from '@/os.js';
|
||||
|
|
@ -96,6 +106,9 @@ const choseAd = (): Ad | null => {
|
|||
};
|
||||
|
||||
const chosen = ref(choseAd());
|
||||
|
||||
const self = computed(() => chosen.value?.url.startsWith(local));
|
||||
|
||||
const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
|
||||
|
||||
function reduceFrequency(): void {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { url as local } from '@/config.js';
|
|||
import * as os from '@/os.js';
|
||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
url: string;
|
||||
|
|
@ -44,12 +45,12 @@ const url = new URL(props.url);
|
|||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
const el = ref();
|
||||
|
||||
if (props.showUrlPreview) {
|
||||
if (props.showUrlPreview && isEnabledUrlPreview.value) {
|
||||
useTooltip(el, (showing) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
|
||||
showing,
|
||||
url: props.url,
|
||||
source: el.value,
|
||||
source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MediaImage
|
||||
v-if="image"
|
||||
:image="image"
|
||||
:disableImageLink="true"
|
||||
/>
|
||||
<div :class="$style.root">
|
||||
<MkMediaList v-if="image" :mediaList="[image]" :class="$style.mediaList"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MediaImage from '@/components/MkMediaImage.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
block: Misskey.entities.PageBlock,
|
||||
|
|
@ -28,5 +24,17 @@ const image = ref<Misskey.entities.DriveFile | null>(null);
|
|||
onMounted(() => {
|
||||
image.value = props.page.attachedFiles.find(x => x.id === props.block.fileId) ?? null;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mediaList {
|
||||
// MkMediaList 内の上部マージン 4px
|
||||
margin-top: -4px;
|
||||
height: calc(100% + 4px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div style="margin: 1em 0;">
|
||||
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
|
||||
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
|
||||
<div :class="$style.root">
|
||||
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" :note="note"/>
|
||||
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" :note="note"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -32,3 +32,10 @@ onMounted(() => {
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps" :class="$style.textRoot">
|
||||
<Mfm :text="block.text ?? ''" :isNote="false"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -15,6 +17,7 @@ import { defineAsyncComponent } from 'vue';
|
|||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
|
||||
import { isEnabledUrlPreview } from '@/instance.js';
|
||||
|
||||
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
|
||||
|
||||
|
|
@ -25,3 +28,9 @@ const props = defineProps<{
|
|||
|
||||
const urls = props.block.text ? extractUrlFromMfm(mfm.parse(props.block.text)) : [];
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.textRoot {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }" class="_gaps_s">
|
||||
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }" class="_gaps">
|
||||
<XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue