Merge branch 'misskey-dev:develop' into error-style

This commit is contained in:
Kainoa Kanter 2022-07-05 15:06:56 -07:00 committed by GitHub
commit 32d50fc8ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1585 additions and 239 deletions

View file

@ -9,6 +9,9 @@ export const meta = {
requireCredential: false,
allowGet: true,
cacheSec: 60,
res: {
type: 'array',
optional: false, nullable: false,

View file

@ -5,7 +5,7 @@ html
head
meta(charset='utf-8')
meta(name='application-name' content='Misskey')
title Misskey BIOS
title Misskey Repair Tool
style
include ../bios.css
script
@ -13,7 +13,7 @@ html
body
header
h1 Misskey BIOS #{version}
h1 Misskey Repair Tool #{version}
main
div.tabs
button#ls edit local storage

View file

@ -1,15 +1,17 @@
<template>
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
<section>
<header class="_acrylic" @click="shown = !shown">
<i class="toggle fa-fw" :class="shown ? 'fas fa-chevron-down' : 'fas fa-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
</header>
<div v-if="shown">
<button v-for="emoji in emojis"
<div v-if="shown" class="body">
<button
v-for="emoji in emojis"
:key="emoji"
class="_button"
class="_button item"
@click="emit('chosen', emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>

View file

@ -3,63 +3,67 @@
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">
<button v-for="emoji in searchResultCustom"
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
:key="emoji.id"
class="_button"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
<img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
<img class="emoji" :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0">
<button v-for="emoji in searchResultUnicode"
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji :emoji="emoji.char"/>
<MkEmoji class="emoji" :emoji="emoji.char"/>
</button>
</div>
</section>
<div v-if="tab === 'index'" class="index">
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div>
<button v-for="emoji in pinned"
<div class="body">
<button
v-for="emoji in pinned"
:key="emoji"
class="_button"
class="_button item"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
<section>
<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header>
<div>
<button v-for="emoji in recentlyUsedEmojis"
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button"
class="_button item"
@click="chosen(emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
<MkEmoji class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
</div>
<div>
<div class="group">
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
</div>
<div>
<div class="group">
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div>
@ -76,6 +80,7 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from './emoji-picker.section.vue';
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Ripple from '@/components/ripple.vue';
@ -83,7 +88,6 @@ import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { deviceKind } from '@/scripts/device-kind';
import { emojiCategories, instance } from '@/instance';
import XSection from './emoji-picker.section.vue';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
@ -266,7 +270,7 @@ watch(q, () => {
function focus() {
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
search.value?.focus({
preventScroll: true
preventScroll: true,
});
}
}
@ -415,19 +419,16 @@ defineExpose({
font-size: 15px;
}
> div {
> .body {
display: grid;
grid-template-columns: var(--columns);
font-size: 30px;
> button {
> .item {
aspect-ratio: 1 / 1;
width: auto;
height: auto;
min-width: 0;
> * {
font-size: 30px;
}
}
}
}
@ -478,7 +479,7 @@ defineExpose({
display: none;
}
> div {
> .group {
&:not(.index) {
padding: 4px 0 8px 0;
border-top: solid 0.5px var(--divider);
@ -513,16 +514,18 @@ defineExpose({
}
}
> div {
> .body {
position: relative;
padding: $pad;
> button {
> .item {
position: relative;
padding: 0;
width: var(--eachSize);
height: var(--eachSize);
contain: strict;
border-radius: 4px;
font-size: 24px;
&:focus-visible {
outline: solid 2px var(--focus);
@ -538,8 +541,7 @@ defineExpose({
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
}
> * {
font-size: 24px;
> .emoji {
height: 1.25em;
vertical-align: -.25em;
pointer-events: none;

View file

@ -178,6 +178,8 @@ onUnmounted(() => {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
border-bottom: solid 0.5px var(--divider);
contain: strict;
height: var(--height);
&.thin {
--height: 45px;

View file

@ -11,7 +11,7 @@ import { decode } from 'blurhash';
const props = withDefaults(defineProps<{
src?: string | null;
hash: string;
hash?: string;
alt?: string;
title?: string | null;
size?: number;

View file

@ -154,7 +154,7 @@ function createDoughnut(chartEl, tooltip, data) {
}
onMounted(() => {
os.apiGet('federation/stats', { limit: 15 }).then(fedStats => {
os.apiGet('federation/stats', { limit: 20 }).then(fedStats => {
createDoughnut(subDoughnutEl, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,

View file

@ -2,9 +2,9 @@
<div v-if="hide" class="qjewsnkg" @click="hide = false">
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div class="text">
<div>
<b><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
<span>{{ $ts.clickToShow }}</span>
<div class="wrapper">
<b style="display: block;"><i class="fas fa-exclamation-triangle"></i> {{ $ts.sensitive }}</b>
<span style="display: block;">{{ $ts.clickToShow }}</span>
</div>
</div>
</div>
@ -37,8 +37,8 @@ let hide = $ref(true);
const url = (props.raw || defaultStore.state.loadRawImages)
? props.image.url
: defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.thumbnailUrl)
: props.image.thumbnailUrl;
? getStaticImageUrl(props.image.thumbnailUrl)
: props.image.thumbnailUrl;
// Plugin:register_note_view_interruptor 使watch
watch(() => props.image, () => {
@ -68,15 +68,11 @@ watch(() => props.image, () => {
justify-content: center;
align-items: center;
> div {
> .wrapper {
display: table-cell;
text-align: center;
font-size: 0.8em;
color: #fff;
> * {
display: block;
}
}
}
}

View file

@ -1,5 +1,5 @@
<template>
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block _isolated" tabindex="-1">
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1">
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
<article>
<header>
@ -23,12 +23,12 @@ export default defineComponent({
props: {
page: {
type: Object,
required: true
required: true,
},
},
methods: {
userName
}
userName,
},
});
</script>

View file

@ -2,7 +2,7 @@
<div v-show="files.length != 0" class="skeikyzd">
<XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
<template #item="{element}">
<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<div class="file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
<div v-if="element.isSensitive" class="sensitive">
<i class="fas fa-exclamation-triangle icon"></i>
@ -22,18 +22,18 @@ import * as os from '@/os';
export default defineComponent({
components: {
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
MkDriveFileThumbnail
MkDriveFileThumbnail,
},
props: {
files: {
type: Array,
required: true
required: true,
},
detachMediaFn: {
type: Function,
required: false
}
required: false,
},
},
emits: ['updated', 'detach', 'changeSensitive', 'changeName'],
@ -51,8 +51,8 @@ export default defineComponent({
},
set(value) {
this.$emit('updated', value);
}
}
},
},
},
methods: {
@ -66,7 +66,7 @@ export default defineComponent({
toggleSensitive(file) {
os.api('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive
isSensitive: !file.isSensitive,
}).then(() => {
this.$emit('changeSensitive', file, !file.isSensitive);
});
@ -75,12 +75,12 @@ export default defineComponent({
const { canceled, result } = await os.inputText({
title: this.$ts.enterFileName,
default: file.name,
allowEmpty: false
allowEmpty: false,
});
if (canceled) return;
os.api('drive/files/update', {
fileId: file.id,
name: result
name: result,
}).then(() => {
this.$emit('changeName', file, result);
file.name = result;
@ -88,13 +88,13 @@ export default defineComponent({
},
async describe(file) {
os.popup(defineAsyncComponent(() => import("@/components/media-caption.vue")), {
os.popup(defineAsyncComponent(() => import('@/components/media-caption.vue')), {
title: this.$ts.describeFile,
input: {
placeholder: this.$ts.inputNewDescription,
default: file.comment !== null ? file.comment : "",
default: file.comment !== null ? file.comment : '',
},
image: file
image: file,
}, {
done: result => {
if (!result || result.canceled) return;
@ -105,7 +105,7 @@ export default defineComponent({
}).then(() => {
file.comment = comment;
});
}
},
}, 'closed');
},
@ -114,22 +114,22 @@ export default defineComponent({
this.menu = os.popupMenu([{
text: this.$ts.renameFile,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file); }
action: () => { this.rename(file); },
}, {
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
action: () => { this.toggleSensitive(file); }
action: () => { this.toggleSensitive(file); },
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: () => { this.describe(file); }
action: () => { this.describe(file); },
}, {
text: this.$ts.attachCancel,
icon: 'fas fa-times-circle',
action: () => { this.detachMedia(file.id); }
action: () => { this.detachMedia(file.id); },
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
}
}
},
},
});
</script>
@ -142,7 +142,7 @@ export default defineComponent({
display: flex;
flex-wrap: wrap;
> div {
> .file {
position: relative;
width: 64px;
height: 64px;

View file

@ -1,5 +1,6 @@
<template>
<div v-size="{ max: [310, 500] }" class="gafaadew"
<div
v-size="{ max: [310, 500] }" class="gafaadew"
:class="{ modal, _popup: modal }"
@dragover.stop="onDragover"
@dragenter="onDragenter"
@ -11,7 +12,7 @@
<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" class="avatar"/>
</button>
<div>
<div class="right">
<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
@ -68,6 +69,8 @@ import * as misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
import { toASCII } from 'punycode/';
import * as Acct from 'misskey-js/built/acct';
import { throttle } from 'throttle-debounce';
import XNoteSimple from './note-simple.vue';
import XNotePreview from './note-preview.vue';
import XPostFormAttaches from './post-form-attaches.vue';
@ -75,14 +78,12 @@ import XPollEditor from './poll-editor.vue';
import { host, url } from '@/config';
import { erase, unique } from '@/scripts/array';
import { extractMentions } from '@/scripts/extract-mentions';
import * as Acct from 'misskey-js/built/acct';
import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete';
import * as os from '@/os';
import { stream } from '@/stream';
import { selectFiles } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
@ -181,7 +182,7 @@ const placeholder = $computed((): string => {
i18n.ts._postForm._placeholders.c,
i18n.ts._postForm._placeholders.d,
i18n.ts._postForm._placeholders.e,
i18n.ts._postForm._placeholders.f
i18n.ts._postForm._placeholders.f,
];
return xs[Math.floor(Math.random() * xs.length)];
}
@ -238,10 +239,10 @@ if (props.reply && props.reply.text != null) {
for (const x of extractMentions(ast)) {
const mention = x.host ?
`@${x.username}@${toASCII(x.host)}` :
(otherHost == null || otherHost === host) ?
`@${x.username}` :
`@${x.username}@${toASCII(otherHost)}`;
`@${x.username}@${toASCII(x.host)}` :
(otherHost == null || otherHost === host) ?
`@${x.username}` :
`@${x.username}@${toASCII(otherHost)}`;
//
if ($i.username === x.username && (x.host == null || x.host === host)) continue;
@ -263,7 +264,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
visibility = props.reply.visibility;
if (props.reply.visibility === 'specified') {
os.api('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
}).then(users => {
users.forEach(pushVisibleUser);
});
@ -399,7 +400,7 @@ function setVisibility() {
if (defaultStore.state.rememberNoteVisibility) {
defaultStore.set('localOnly', localOnly);
}
}
},
}, 'closed');
}
@ -522,8 +523,8 @@ function saveDraft() {
visibility: visibility,
localOnly: localOnly,
files: files,
poll: poll
}
poll: poll,
},
};
localStorage.setItem('drafts', JSON.stringify(draftData));
@ -612,11 +613,11 @@ function showActions(ev) {
text: action.title,
action: () => {
action.handler({
text: text
text: text,
}, (key, value) => {
if (key === 'text') { text = value; }
});
}
},
})), ev.currentTarget ?? ev.target);
}
@ -726,7 +727,7 @@ onMounted(() => {
}
}
> div {
> .right {
position: absolute;
top: 0;
right: 0;
@ -924,7 +925,7 @@ onMounted(() => {
line-height: 50px;
}
> div {
> .right {
> .text-count {
line-height: 50px;
}

View file

@ -70,10 +70,11 @@ onMounted(() => {
});
useTooltip(buttonRef, async (showing) => {
const reactions = await os.api('notes/reactions', {
const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
limit: 11,
_cacheKey_: props.count,
});
const users = reactions.map(x => x.user);

View file

@ -1,5 +1,5 @@
<template>
<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
</template>
<script lang="ts" setup>
@ -15,7 +15,7 @@ defineProps<{
background: var(--infoWarnBg);
color: var(--infoWarnFg);
> a {
> .link {
margin-left: 4px;
color: var(--accent);
}

View file

@ -144,6 +144,7 @@ export default defineComponent({
.ukygtjoj {
position: relative;
overflow: hidden; overflow: clip;
contain: content;
&.naked {
background: transparent !important;

View file

@ -133,8 +133,10 @@ const fetchMore = async (): Promise<void> => {
limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : props.pagination.reversed ? {
sinceId: items.value[0].id,
} : {
untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
untilId: items.value[items.value.length - 1].id,
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
@ -169,8 +171,10 @@ const fetchMoreAhead = async (): Promise<void> => {
limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : props.pagination.reversed ? {
untilId: items.value[0].id,
} : {
sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
sinceId: items.value[items.value.length - 1].id,
}),
}).then(res => {
if (res.length > SECOND_FETCH_LIMIT) {

View file

@ -1,14 +1,14 @@
<template>
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
<button class="disablePlayer" :title="$ts.disablePlayer" @click="playerEnabled = false"><i class="fas fa-times"></i></button>
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
</div>
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
<button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button>
</div>
@ -57,7 +57,7 @@ let sitename = $ref<string | null>(null);
let player = $ref({
url: null,
width: null,
height: null
height: null,
});
let playerEnabled = $ref(false);
let tweetId = $ref<string | null>(null);
@ -143,7 +143,7 @@ onUnmounted(() => {
.mk-url-preview {
&.max-width_400px {
> a {
> .link {
font-size: 12px;
> .thumbnail {
@ -157,7 +157,7 @@ onUnmounted(() => {
}
&.max-width_350px {
> a {
> .link {
font-size: 10px;
> .thumbnail {
@ -205,7 +205,7 @@ onUnmounted(() => {
}
}
> a {
> .link {
position: relative;
display: block;
font-size: 14px;

View file

@ -113,6 +113,7 @@ export default defineComponent({
}
> .widget, .customize-container {
contain: content;
margin: var(--margin) 0;
&:first-of-type {

View file

@ -164,4 +164,11 @@ export const menuDef = reactive({
}], ev.currentTarget ?? ev.target);
},
},
reload: {
title: 'reload',
icon: 'fas fa-refresh',
action: (ev) => {
location.reload();
},
},
});

View file

@ -61,6 +61,7 @@ export class Router extends EventEmitter<{
props: Map<string, string> | null;
key: string;
}) => void;
same: () => void;
}> {
private routes: RouteDef[];
private currentPath: string;
@ -210,11 +211,15 @@ export class Router extends EventEmitter<{
}
public push(path: string) {
const beforePath = this.currentPath;
if (path === beforePath) {
this.emit('same');
return;
}
if (this.navHook) {
const cancel = this.navHook(path);
if (cancel) return;
}
const beforePath = this.currentPath;
this.navigate(path, null);
this.emit('push', {
beforePath,

View file

@ -12,19 +12,21 @@
<XUsers origin="remote"/>
</div>
<div v-else-if="tab === 'search'">
<div class="_isolated">
<MkInput v-model="searchQuery" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.searchUser }}</template>
</MkInput>
<MkRadios v-model="searchOrigin">
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkRadios>
</div>
<MkSpacer :content-max="1200">
<div>
<MkInput v-model="searchQuery" :debounce="true" type="search" class="_formBlock">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.searchUser }}</template>
</MkInput>
<MkRadios v-model="searchOrigin" class="_formBlock">
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkRadios>
</div>
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
<XUserList v-if="searchQuery" ref="searchEl" class="_gap" :pagination="searchPagination"/>
</MkSpacer>
</div>
</div>
</MkStickyContainer>
@ -42,6 +44,7 @@ import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import XUserList from '@/components/user-list.vue';
const props = defineProps<{
tag?: string;

View file

@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<div class="mwysmxbg">
<div class="_isolated">{{ $ts._mfm.intro }}</div>
<div>{{ $ts._mfm.intro }}</div>
<div class="section _block">
<div class="title">{{ $ts._mfm.mention }}</div>
<div class="content">

View file

@ -1,41 +1,43 @@
<template><MkStickyContainer>
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div class="fcuexfpr">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note">
<div v-if="showNext" class="_gap">
<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
</div>
<div class="main _gap">
<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
<div class="note _gap">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri" class="_isolated"/>
<XNoteDetailed :key="note.id" v-model:note="note" class="_isolated note"/>
<MkSpacer :content-max="800">
<div class="fcuexfpr">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note">
<div v-if="showNext" class="_gap">
<XNotes class="_content" :pagination="nextPagination" :no-gap="true"/>
</div>
<div v-if="clips && clips.length > 0" class="_content clips _gap">
<div class="title">{{ $ts.clip }}</div>
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
<div class="user">
<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
</div>
</MkA>
</div>
<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
</div>
<div v-if="showPrev" class="_gap">
<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
<div class="main _gap">
<MkButton v-if="!showNext && hasNext" class="load next" @click="showNext = true"><i class="fas fa-chevron-up"></i></MkButton>
<div class="note _gap">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
<XNoteDetailed :key="note.id" v-model:note="note" class="note"/>
</div>
<div v-if="clips && clips.length > 0" class="_content clips _gap">
<div class="title">{{ $ts.clip }}</div>
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
<div class="user">
<MkAvatar :user="item.user" class="avatar" :show-indicator="true"/> <MkUserName :user="item.user" :nowrap="false"/>
</div>
</MkA>
</div>
<MkButton v-if="!showPrev && hasPrev" class="load prev" @click="showPrev = true"><i class="fas fa-chevron-down"></i></MkButton>
</div>
<div v-if="showPrev" class="_gap">
<XNotes class="_content" :pagination="prevPagination" :no-gap="true"/>
</div>
</div>
</div>
<MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/>
</transition>
</div>
</MkSpacer></MkStickyContainer>
<MkError v-else-if="error" @retry="fetch()"/>
<MkLoading v-else/>
</transition>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>

View file

@ -1,7 +1,7 @@
<template>
<div>
<MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers">
<div class="users _isolated">
<div class="users">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
</div>
</MkPagination>

View file

@ -258,6 +258,10 @@ mainRouter.addListener('push', ctx => {
}
});
mainRouter.addListener('same', () => {
window.scroll({ top: 0, behavior: 'smooth' });
});
window.addEventListener('popstate', (event) => {
mainRouter.change(location.pathname + location.search + location.hash, event.state?.key);
const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;

View file

@ -1,32 +1,32 @@
<template>
<div class="kmwsukvl">
<div>
<div class="body">
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
<i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
</button>
<MkA v-click-anime class="item" active-class="active" to="/settings">
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
<button class="item _button post" data-cy-open-post-form @click="post">
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
</button>
</div>
</div>
@ -81,7 +81,7 @@ export default defineComponent({
$avatar-size: 32px;
$avatar-margin: 8px;
> div {
> .body {
> .divider {
margin: 16px 16px;
@ -102,12 +102,12 @@ export default defineComponent({
box-sizing: border-box;
color: var(--navFg);
> i {
> .icon {
position: relative;
width: 32px;
}
> i,
> .icon,
> .avatar {
margin-right: $avatar-margin;
}

View file

@ -1,32 +1,32 @@
<template>
<div class="mvcprjjd" :class="{ iconOnly }">
<div>
<div class="body">
<button v-click-anime class="item _button account" @click="openAccountMenu">
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
<MkA v-click-anime class="item index" active-class="active" to="/" exact>
<i class="fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
<i class="icon fas fa-home fa-fw"></i><span class="text">{{ $ts.timeline }}</span>
</MkA>
<template v-for="item in menu">
<div v-if="item === '-'" class="divider"></div>
<component :is="menuDef[item].to ? 'MkA' : 'button'" v-else-if="menuDef[item] && (menuDef[item].show !== false)" v-click-anime class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" :to="menuDef[item].to" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}">
<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span>
<i class="icon fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span>
<span v-if="menuDef[item].indicated" class="indicator"><i class="icon fas fa-circle"></i></span>
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" active-class="active" to="/admin">
<i class="fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
<i class="icon fas fa-door-open fa-fw"></i><span class="text">{{ $ts.controlPanel }}</span>
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="fas fa-circle"></i></span>
<i class="icon fa fa-ellipsis-h fa-fw"></i><span class="text">{{ $ts.more }}</span>
<span v-if="otherMenuItemIndicated" class="indicator"><i class="icon fas fa-circle"></i></span>
</button>
<MkA v-click-anime class="item" active-class="active" to="/settings">
<i class="fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
<i class="icon fas fa-cog fa-fw"></i><span class="text">{{ $ts.settings }}</span>
</MkA>
<button class="item _button post" data-cy-open-post-form @click="os.post">
<i class="fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
<i class="icon fas fa-pencil-alt fa-fw"></i><span class="text">{{ $ts.note }}</span>
</button>
</div>
</div>
@ -88,7 +88,7 @@ function more(ev: MouseEvent) {
width: $nav-width;
box-sizing: border-box;
> div {
> .body {
position: fixed;
top: 0;
left: 0;
@ -100,6 +100,7 @@ function more(ev: MouseEvent) {
overflow: auto;
overflow-x: clip;
background: var(--navBg);
contain: strict;
> .divider {
margin: 16px 16px;
@ -120,12 +121,12 @@ function more(ev: MouseEvent) {
box-sizing: border-box;
color: var(--navFg);
> i {
> .icon {
position: relative;
width: 32px;
}
> i,
> .icon,
> .avatar {
margin-right: $avatar-margin;
}
@ -230,7 +231,7 @@ function more(ev: MouseEvent) {
flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width;
> div {
> .body {
width: $nav-icon-only-width;
> .divider {
@ -246,13 +247,13 @@ function more(ev: MouseEvent) {
font-size: $ui-font-size * 1.1;
line-height: initial;
> i,
> .icon,
> .avatar {
display: block;
margin: 0 auto;
}
> i {
> .icon {
opacity: 0.7;
}
@ -261,7 +262,7 @@ function more(ev: MouseEvent) {
}
&:hover, &.active {
> i, > .text {
> .icon, > .text {
opacity: 1;
}
}
@ -284,7 +285,7 @@ function more(ev: MouseEvent) {
&.post {
height: $nav-icon-only-width;
> i {
> .icon {
opacity: 1;
}
}