Merge tag '13.6.1' into io
This commit is contained in:
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
<footer :class="$style.footer">
|
||||
<MkReactionsViewer :note="appearNote" :max-number="16">
|
||||
<template v-slot:more>
|
||||
<template #more>
|
||||
<button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
|
||||
{{ i18n.ts.more }}
|
||||
</button>
|
||||
@@ -156,6 +156,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
import { shownNoteIds } from '@/os';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -206,7 +207,7 @@ const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
let renoteCollapsed = $ref(isRenote && (($i && ($i.id === note.userId)) || shownNoteIds.has(appearNote.id)));
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId)) || shownNoteIds.has(appearNote.id)));
|
||||
|
||||
shownNoteIds.add(appearNote.id);
|
||||
|
||||
@@ -247,7 +248,32 @@ useTooltip(renoteButton, async (showing) => {
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
|
||||
let items = [] as MenuItem[];
|
||||
|
||||
if (appearNote.channel) {
|
||||
items = items.concat([{
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.inChannelQuote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
channel: appearNote.channel,
|
||||
});
|
||||
},
|
||||
}, null]);
|
||||
}
|
||||
|
||||
items = items.concat([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
@@ -263,7 +289,9 @@ function renote(viaKeyboard = false) {
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
}]);
|
||||
|
||||
os.popupMenu(items, renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
@@ -704,6 +732,12 @@ function showReactions(): void {
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 250px) {
|
||||
.quoteNote {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
|
@@ -160,6 +160,7 @@ import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { claimAchievement } from '@/scripts/achievements';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -241,7 +242,32 @@ useTooltip(renoteButton, async (showing) => {
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
|
||||
let items = [] as MenuItem[];
|
||||
|
||||
if (appearNote.channel) {
|
||||
items = items.concat([{
|
||||
text: i18n.ts.inChannelRenote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
channelId: appearNote.channelId,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.inChannelQuote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
channel: appearNote.channel,
|
||||
});
|
||||
},
|
||||
}, null]);
|
||||
}
|
||||
|
||||
items = items.concat([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
@@ -257,7 +283,9 @@ function renote(viaKeyboard = false) {
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
}]);
|
||||
|
||||
os.popupMenu(items, renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
@@ -44,8 +44,8 @@ const showContent = $ref(false);
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
margin: 0 10px 0 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -72,6 +72,14 @@ const showContent = $ref(false);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@container (min-width: 250px) {
|
||||
.avatar {
|
||||
margin: 0 10px 0 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 350px) {
|
||||
.avatar {
|
||||
margin: 0 10px 0 0;
|
||||
|
@@ -21,7 +21,7 @@
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="_margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
<MkButton v-if="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? fetchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
|
234
packages/frontend/src/components/global/MkPageHeader.tabs.vue
Normal file
234
packages/frontend/src/components/global/MkPageHeader.tabs.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div ref="el" :class="$style.tabs" @wheel="onTabWheel">
|
||||
<div :class="$style.tabsInner">
|
||||
<button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
|
||||
class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
|
||||
@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)">
|
||||
<div :class="$style.tabInner">
|
||||
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
|
||||
<div v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)"
|
||||
:class="$style.tabTitle">{{ t.title }}</div>
|
||||
<Transition v-else @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave"
|
||||
mode="in-out">
|
||||
<div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="tabHighlightEl"
|
||||
:class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type Tab = {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
} & {
|
||||
iconOnly: true;
|
||||
iccn: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, watch, nextTick, Transition, shallowRef } from 'vue';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
tab?: string;
|
||||
rootEl?: HTMLElement;
|
||||
}>(), {
|
||||
tabs: () => ([] as Tab[]),
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:tab', key: string);
|
||||
(ev: 'tabClick', key: string);
|
||||
}>();
|
||||
|
||||
const el = shallowRef<HTMLElement | null>(null);
|
||||
const tabRefs: Record<string, HTMLElement | null> = {};
|
||||
const tabHighlightEl = shallowRef<HTMLElement | null>(null);
|
||||
|
||||
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
|
||||
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
|
||||
if (tab.key) {
|
||||
emit('update:tab', tab.key);
|
||||
}
|
||||
}
|
||||
|
||||
function onTabClick(t: Tab, ev: MouseEvent): void {
|
||||
emit('tabClick', t.key);
|
||||
|
||||
if (t.onClick) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
t.onClick(ev);
|
||||
}
|
||||
|
||||
if (t.key) {
|
||||
emit('update:tab', t.key);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTab() {
|
||||
const tabEl = props.tab ? tabRefs[props.tab] : undefined;
|
||||
if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabHighlightEl.value.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.value.style.width = rect.width + 'px';
|
||||
tabHighlightEl.value.style.left = (rect.left - parentRect.left + tabHighlightEl.value.parentElement.scrollLeft) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onTabWheel(ev: WheelEvent) {
|
||||
if (ev.deltaY !== 0 && ev.deltaX === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as HTMLElement).scrollBy({
|
||||
left: ev.deltaY,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let entering = false;
|
||||
|
||||
async function enter(el: HTMLElement) {
|
||||
entering = true;
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = '0';
|
||||
el.style.paddingLeft = '0';
|
||||
el.offsetWidth; // force reflow
|
||||
el.style.width = elementWidth + 'px';
|
||||
el.style.paddingLeft = '';
|
||||
nextTick(() => {
|
||||
entering = false;
|
||||
});
|
||||
|
||||
setTimeout(renderTab, 170);
|
||||
}
|
||||
function afterEnter(el: HTMLElement) {
|
||||
//el.style.width = '';
|
||||
}
|
||||
async function leave(el: HTMLElement) {
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = elementWidth + 'px';
|
||||
el.style.paddingLeft = '';
|
||||
el.offsetWidth; // force reflow
|
||||
el.style.width = '0';
|
||||
el.style.paddingLeft = '0';
|
||||
}
|
||||
function afterLeave(el: HTMLElement) {
|
||||
el.style.width = '';
|
||||
}
|
||||
|
||||
let ro2: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
watch([() => props.tab, () => props.tabs], () => {
|
||||
nextTick(() => {
|
||||
if (entering) return;
|
||||
renderTab();
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
if (props.rootEl) {
|
||||
ro2 = new ResizeObserver((entries, observer) => {
|
||||
if (document.body.contains(el.value as HTMLElement)) {
|
||||
nextTick(() => renderTab());
|
||||
}
|
||||
});
|
||||
ro2.observe(props.rootEl);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ro2) ro2.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tabs {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
height: var(--height);
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsInner {
|
||||
display: inline-block;
|
||||
height: var(--height);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.animate {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.tabInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabIcon+.tabTitle {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.tabTitle {
|
||||
overflow: hidden;
|
||||
|
||||
&.animate {
|
||||
transition: width .15s linear, padding-left .15s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.tabHighlight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: none;
|
||||
pointer-events: none;
|
||||
|
||||
&.animate {
|
||||
transition: width 0.15s ease, left 0.15s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div v-if="show" ref="el" :class="[$style.root]" :style="{ background: bg }">
|
||||
<div :class="[$style.upper, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div v-if="narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<div v-if="!thin_ && narrow && props.displayMyAvatar && $i" class="_button" :class="$style.buttonsLeft" @click="openAccountMenu">
|
||||
<MkAvatar :class="$style.avatar" :user="$i" />
|
||||
</div>
|
||||
<div v-else-if="narrow && !hideTitle" :class="$style.buttonsLeft" />
|
||||
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft" />
|
||||
|
||||
<template v-if="metadata">
|
||||
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
|
||||
@@ -19,63 +19,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!narrow || hideTitle" :class="$style.tabs" @wheel="onTabWheel">
|
||||
<div :class="$style.tabsInner">
|
||||
<button v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title" class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab }]" @mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)">
|
||||
<div :class="$style.tabInner">
|
||||
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
|
||||
<div v-if="!t.iconOnly" :class="$style.tabTitle">{{ t.title }}</div>
|
||||
<Transition
|
||||
v-else
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
mode="in-out"
|
||||
>
|
||||
<div v-if="t.key === tab" :class="$style.tabTitle">{{ t.title }}</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="tabHighlightEl" :class="$style.tabHighlight"></div>
|
||||
</div>
|
||||
<XTabs v-if="!narrow || hideTitle" :class="$style.tabs" :tab="tab" @update:tab="key => emit('update:tab', key)" :tabs="tabs" :root-el="el" @tab-click="onTabClick"/>
|
||||
</template>
|
||||
<div v-if="(narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttonsRight">
|
||||
<div v-if="(!thin_ && narrow && !hideTitle) || (actions && actions.length > 0)" :class="$style.buttonsRight">
|
||||
<template v-for="action in actions">
|
||||
<button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="(narrow && !hideTitle) && hasTabs" :class="[$style.lower, { [$style.slim]: narrow, [$style.thin]: thin_ }]">
|
||||
<div :class="$style.tabs" @wheel="onTabWheel">
|
||||
<div :class="$style.tabsInner">
|
||||
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="_button" :class="[$style.tab, { [$style.active]: tab.key != null && tab.key === props.tab }]" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
|
||||
<i v-if="tab.icon" :class="[$style.tabIcon, tab.icon]"></i>
|
||||
<span v-if="!tab.iconOnly" :class="$style.tabTitle">{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="tabHighlightEl" :class="$style.tabHighlight"></div>
|
||||
</div>
|
||||
<XTabs :class="$style.tabs" :tab="tab" @update:tab="key => emit('update:tab', key)" :tabs="tabs" :root-el="el" @tab-click="onTabClick"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, inject, watch, nextTick } from 'vue';
|
||||
import { onMounted, onUnmounted, ref, inject } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { scrollToTop } from '@/scripts/scroll';
|
||||
import { globalEvents } from '@/events';
|
||||
import { injectPageMetadata } from '@/scripts/page-metadata';
|
||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
|
||||
type Tab = {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
iconOnly?: boolean;
|
||||
onClick?: (ev: MouseEvent) => void;
|
||||
};
|
||||
import XTabs, { Tab } from './MkPageHeader.tabs.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
tabs?: Tab[];
|
||||
@@ -102,8 +67,6 @@ const hideTitle = inject('shouldOmitHeaderTitle', false);
|
||||
const thin_ = props.thin || inject('shouldHeaderThin', false);
|
||||
|
||||
let el = $shallowRef<HTMLElement | undefined>(undefined);
|
||||
const tabRefs: Record<string, HTMLElement | null> = {};
|
||||
let tabHighlightEl = $shallowRef<HTMLElement | null>(null);
|
||||
const bg = ref<string | undefined>(undefined);
|
||||
let narrow = $ref(false);
|
||||
const hasTabs = $computed(() => props.tabs.length > 0);
|
||||
@@ -128,25 +91,8 @@ function openAccountMenu(ev: MouseEvent) {
|
||||
}, ev);
|
||||
}
|
||||
|
||||
function onTabMousedown(tab: Tab, ev: MouseEvent): void {
|
||||
// ユーザビリティの観点からmousedown時にはonClickは呼ばない
|
||||
if (tab.key) {
|
||||
emit('update:tab', tab.key);
|
||||
}
|
||||
}
|
||||
|
||||
function onTabClick(t: Tab, ev: MouseEvent): void {
|
||||
if (t.key === props.tab) {
|
||||
top();
|
||||
} else if (t.onClick) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
t.onClick(ev);
|
||||
}
|
||||
|
||||
if (t.key) {
|
||||
emit('update:tab', t.key);
|
||||
}
|
||||
function onTabClick(): void {
|
||||
top();
|
||||
}
|
||||
|
||||
const calcBg = () => {
|
||||
@@ -156,88 +102,26 @@ const calcBg = () => {
|
||||
bg.value = tinyBg.toRgbString();
|
||||
};
|
||||
|
||||
let ro1: ResizeObserver | null;
|
||||
let ro2: ResizeObserver | null;
|
||||
|
||||
function renderTab() {
|
||||
const tabEl = props.tab ? tabRefs[props.tab] : undefined;
|
||||
if (tabEl && tabHighlightEl && tabHighlightEl.parentElement) {
|
||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||
const parentRect = tabHighlightEl.parentElement.getBoundingClientRect();
|
||||
const rect = tabEl.getBoundingClientRect();
|
||||
tabHighlightEl.style.width = rect.width + 'px';
|
||||
tabHighlightEl.style.left = (rect.left - parentRect.left + tabHighlightEl.parentElement.scrollLeft) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onTabWheel(ev: WheelEvent) {
|
||||
if (ev.deltaY !== 0 && ev.deltaX === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
(ev.currentTarget as HTMLElement).scrollBy({
|
||||
left: ev.deltaY,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function enter(el: HTMLElement) {
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = '0';
|
||||
el.offsetWidth; // reflow
|
||||
el.style.width = elementWidth + 'px';
|
||||
setTimeout(renderTab, 70);
|
||||
}
|
||||
function afterEnter(el: HTMLElement) {
|
||||
el.style.width = '';
|
||||
nextTick(renderTab);
|
||||
}
|
||||
function leave(el: HTMLElement) {
|
||||
const elementWidth = el.getBoundingClientRect().width;
|
||||
el.style.width = elementWidth + 'px';
|
||||
el.offsetWidth; // reflow
|
||||
el.style.width = '0';
|
||||
}
|
||||
function afterLeave(el: HTMLElement) {
|
||||
el.style.width = '';
|
||||
}
|
||||
let ro: ResizeObserver | null;
|
||||
|
||||
onMounted(() => {
|
||||
calcBg();
|
||||
globalEvents.on('themeChanged', calcBg);
|
||||
|
||||
watch([() => props.tab, () => props.tabs], () => {
|
||||
nextTick(() => renderTab());
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
if (el && el.parentElement) {
|
||||
narrow = el.parentElement.offsetWidth < 500;
|
||||
ro1 = new ResizeObserver((entries, observer) => {
|
||||
ro = new ResizeObserver((entries, observer) => {
|
||||
if (el && el.parentElement && document.body.contains(el as HTMLElement)) {
|
||||
narrow = el.parentElement.offsetWidth < 500;
|
||||
}
|
||||
});
|
||||
ro1.observe(el.parentElement as HTMLElement);
|
||||
}
|
||||
|
||||
if (el) {
|
||||
ro2 = new ResizeObserver((entries, observer) => {
|
||||
if (document.body.contains(el as HTMLElement)) {
|
||||
nextTick(() => renderTab());
|
||||
}
|
||||
});
|
||||
ro2.observe(el);
|
||||
ro.observe(el.parentElement as HTMLElement);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
globalEvents.off('themeChanged', calcBg);
|
||||
if (ro1) ro1.disconnect();
|
||||
if (ro2) ro2.disconnect();
|
||||
if (ro) ro.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -258,6 +142,7 @@ onUnmounted(() => {
|
||||
.upper {
|
||||
--height: 50px;
|
||||
display: flex;
|
||||
gap: var(--margin);
|
||||
height: var(--height);
|
||||
|
||||
.tabs:first-child {
|
||||
@@ -267,12 +152,9 @@ onUnmounted(() => {
|
||||
padding-left: 16px;
|
||||
mask-image: linear-gradient(90deg, rgba(0,0,0,0), rgb(0,0,0) 16px, rgb(0,0,0) 100%);
|
||||
}
|
||||
.tabs:last-child {
|
||||
.tabs {
|
||||
margin-right: auto;
|
||||
}
|
||||
.tabs:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.thin {
|
||||
--height: 42px;
|
||||
@@ -286,19 +168,14 @@ onUnmounted(() => {
|
||||
|
||||
&.slim {
|
||||
text-align: center;
|
||||
gap: 0;
|
||||
|
||||
.tabs:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
> .titleContainer {
|
||||
flex: 1;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
|
||||
> *:first-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
> *:last-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,8 +191,6 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
min-width: var(--height);
|
||||
height: var(--height);
|
||||
margin: 0 var(--margin);
|
||||
|
||||
&:empty {
|
||||
width: var(--height);
|
||||
}
|
||||
@@ -323,12 +198,12 @@ onUnmounted(() => {
|
||||
|
||||
.buttonsLeft {
|
||||
composes: buttons;
|
||||
margin-right: auto;
|
||||
margin: 0 var(--margin) 0 0;
|
||||
}
|
||||
|
||||
.buttonsRight {
|
||||
composes: buttons;
|
||||
margin-left: auto;
|
||||
margin: 0 0 0 var(--margin);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -373,7 +248,7 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
@@ -418,68 +293,4 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
height: var(--height);
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabsInner {
|
||||
display: inline-block;
|
||||
height: var(--height);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-weight: normal;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tabInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabIcon + .tabTitle {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tabTitle {
|
||||
overflow: hidden;
|
||||
transition: width 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.tabHighlight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
transition: width 0.15s ease, left 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
@@ -17,6 +17,7 @@ import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
|
||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
|
||||
import MkContextMenu from '@/components/MkContextMenu.vue';
|
||||
import { MenuItem } from '@/types/menu';
|
||||
import copyToClipboard from './scripts/copy-to-clipboard';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
@@ -26,10 +27,32 @@ export const apiWithDialog = ((
|
||||
token?: string | null | undefined,
|
||||
) => {
|
||||
const promise = api(endpoint, data, token);
|
||||
promiseDialog(promise, null, (err) => {
|
||||
promiseDialog(promise, null, async (err) => {
|
||||
let title = null;
|
||||
let text = err.message + '\n' + (err as any).id;
|
||||
if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
if (err.code === 'INTERNAL_ERROR') {
|
||||
title = i18n.ts.internalServerError;
|
||||
text = i18n.ts.internalServerErrorDescription;
|
||||
const date = new Date().toISOString();
|
||||
const { result } = await actions({
|
||||
type: 'error',
|
||||
title,
|
||||
text,
|
||||
actions: [{
|
||||
value: 'ok',
|
||||
text: i18n.ts.gotIt,
|
||||
primary: true,
|
||||
}, {
|
||||
value: 'copy',
|
||||
text: i18n.ts.copyErrorInfo,
|
||||
}],
|
||||
});
|
||||
if (result === 'copy') {
|
||||
copyToClipboard(`Endpoint: ${endpoint}\nInfo: ${JSON.stringify(err.info)}\nDate: ${date}`);
|
||||
success();
|
||||
}
|
||||
return;
|
||||
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
title = i18n.ts.cannotPerformTemporary;
|
||||
text = i18n.ts.cannotPerformTemporaryDescription;
|
||||
} else if (err.code.startsWith('TOO_MANY')) {
|
||||
|
@@ -60,11 +60,17 @@ import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let ads: any[] = $ref([]);
|
||||
|
||||
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
|
||||
const localTime = new Date();
|
||||
const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
|
||||
|
||||
os.api('admin/ad/list').then(adsResponse => {
|
||||
ads = adsResponse.map(r => {
|
||||
const date = new Date(r.expiresAt);
|
||||
date.setMilliseconds(date.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...r,
|
||||
expiresAt: new Date(r.expiresAt).toISOString().slice(0, 16),
|
||||
expiresAt: date.toISOString().slice(0, 16),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@@ -72,7 +72,7 @@
|
||||
<MkSwitch v-model="policies.rateLimitFactor.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange :model-value="policies.rateLimitFactor.value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor.value = (v / 100)">
|
||||
<MkRange :model-value="policies.rateLimitFactor.value * 100" :min="0" :max="400" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor.value = (v / 100)">
|
||||
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||
</MkRange>
|
||||
|
@@ -45,6 +45,7 @@
|
||||
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||
<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
|
||||
<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
|
||||
<MkSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</MkSwitch>
|
||||
@@ -139,6 +140,7 @@ async function reloadAsk() {
|
||||
|
||||
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
|
||||
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
|
||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
|
||||
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
|
||||
|
@@ -32,6 +32,7 @@ import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
@@ -57,7 +58,7 @@ function queueUpdated(q: number): void {
|
||||
}
|
||||
|
||||
function top(): void {
|
||||
scroll(rootEl, { top: 0 });
|
||||
if (rootEl) scroll(rootEl, { top: 0 });
|
||||
}
|
||||
|
||||
async function chooseList(ev: MouseEvent): Promise<void> {
|
||||
@@ -150,7 +151,7 @@ const headerTabs = $computed(() => [{
|
||||
title: i18n.ts.channel,
|
||||
iconOnly: true,
|
||||
onClick: chooseChannel,
|
||||
}]);
|
||||
}] as Tab[]);
|
||||
|
||||
const headerTabsWhenNotLogin = $computed(() => [
|
||||
...(isLocalTimelineAvailable ? [{
|
||||
@@ -165,7 +166,7 @@ const headerTabsWhenNotLogin = $computed(() => [
|
||||
icon: 'ti ti-whirl',
|
||||
iconOnly: true,
|
||||
}] : []),
|
||||
]);
|
||||
] as Tab[]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
title: i18n.ts.timeline,
|
||||
|
@@ -75,15 +75,15 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="status">
|
||||
<MkA v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
|
||||
<MkA v-click-anime :to="userPage(user)">
|
||||
<b>{{ number(user.notesCount) }}</b>
|
||||
<span>{{ i18n.ts.notes }}</span>
|
||||
</MkA>
|
||||
<MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
|
||||
<MkA v-click-anime :to="userPage(user, 'following')">
|
||||
<b>{{ number(user.followingCount) }}</b>
|
||||
<span>{{ i18n.ts.following }}</span>
|
||||
</MkA>
|
||||
<MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
|
||||
<MkA v-click-anime :to="userPage(user, 'followers')">
|
||||
<b>{{ number(user.followersCount) }}</b>
|
||||
<span>{{ i18n.ts.followers }}</span>
|
||||
</MkA>
|
||||
@@ -100,6 +100,7 @@
|
||||
<XPhotos :key="user.id" :user="user"/>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
</template>
|
||||
<XNotes :no-gap="true" :pagination="pagination"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||
@@ -132,6 +133,7 @@ import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { dateString } from '@/filters/date';
|
||||
import { confetti } from '@/scripts/confetti';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
|
||||
const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
||||
@@ -148,6 +150,14 @@ let narrow = $ref<null | boolean>(null);
|
||||
let rootEl = $ref<null | HTMLElement>(null);
|
||||
let bannerEl = $ref<null | HTMLElement>(null);
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/notes' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
};
|
||||
|
||||
const style = $computed(() => {
|
||||
if (props.user.bannerUrl == null) return {};
|
||||
return {
|
||||
|
@@ -6,6 +6,7 @@
|
||||
<div v-if="user">
|
||||
<XHome v-if="tab === 'home'" :user="user"/>
|
||||
<XTimeline v-else-if="tab === 'notes'" :user="user" />
|
||||
<XActivity v-else-if="tab === 'activity'" :user="user"/>
|
||||
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
|
||||
<XClips v-else-if="tab === 'clips'" :user="user"/>
|
||||
@@ -20,13 +21,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, computed, inject, onMounted, onUnmounted, watch } from 'vue';
|
||||
import calcAge from 's-age';
|
||||
import { defineAsyncComponent, computed, watch } from 'vue';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { getScrollPosition } from '@/scripts/scroll';
|
||||
import number from '@/filters/number';
|
||||
import { userPage, acct as getAcct } from '@/filters/user';
|
||||
import { acct as getAcct } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
@@ -49,8 +47,6 @@ const props = withDefaults(defineProps<{
|
||||
page: 'home',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let tab = $ref(props.page);
|
||||
let user = $ref<null | misskey.entities.UserDetailed>(null);
|
||||
let error = $ref(null);
|
||||
|
@@ -46,6 +46,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
collapseRenotes: {
|
||||
where: 'account',
|
||||
default: true,
|
||||
},
|
||||
rememberNoteVisibility: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
|
Reference in New Issue
Block a user