summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-03-03 15:35:40 +0900
committerGitHub <noreply@github.com>2023-03-03 15:35:40 +0900
commit5bd68aa3e0ce65a9183e193ff113b2486021f679 (patch)
tree7cce90cfd87cbdfc932226273a6a89aa7871b33e /packages/frontend/src/components
parentMerge pull request #10112 from misskey-dev/develop (diff)
parentfix CHANGELOG.md (diff)
downloadmisskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.tar.gz
misskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.tar.bz2
misskey-5bd68aa3e0ce65a9183e193ff113b2486021f679.zip
Merge pull request #10177 from misskey-dev/develop
Release: 13.9.0
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue4
-rw-r--r--packages/frontend/src/components/MkFolder.vue4
-rw-r--r--packages/frontend/src/components/MkMediaList.vue23
-rw-r--r--packages/frontend/src/components/MkMenu.vue2
-rw-r--r--packages/frontend/src/components/MkNote.vue119
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue25
-rw-r--r--packages/frontend/src/components/MkNoteHeader.vue1
-rw-r--r--packages/frontend/src/components/MkPostForm.vue10
-rw-r--r--packages/frontend/src/components/MkTab.vue2
-rw-r--r--packages/frontend/src/components/MkToast.vue2
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue222
-rw-r--r--packages/frontend/src/components/MkUserPreview.vue199
-rw-r--r--packages/frontend/src/components/MkWindow.vue4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue2
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue42
15 files changed, 399 insertions, 262 deletions
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index d1b5cc5118..043a614e46 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -18,7 +18,7 @@
</div>
</Transition>
<div class="container">
- <img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
+ <img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
</div>
</div>
</template>
@@ -49,7 +49,7 @@ const props = defineProps<{
aspectRatio: number;
}>();
-const imgUrl = getProxiedImageUrl(props.file.url);
+const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
let imgEl = $shallowRef<HTMLImageElement>();
let cropper: Cropper | null = null;
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index b97e36cd5f..a54a1c2305 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -43,8 +43,8 @@
import { nextTick, onMounted } from 'vue';
const props = withDefaults(defineProps<{
- defaultOpen: boolean;
- maxHeight: number | null;
+ defaultOpen?: boolean;
+ maxHeight?: number | null;
}>(), {
defaultOpen: false,
maxHeight: null,
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index fafa0bd232..c768a086cd 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -13,7 +13,7 @@
</template>
<script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, useCssModule } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
@@ -29,8 +29,11 @@ const props = defineProps<{
raw?: boolean;
}>();
+const $style = useCssModule();
+
const gallery = ref(null);
const pswpZIndex = os.claimZIndex('middle');
+document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
onMounted(() => {
@@ -54,17 +57,18 @@ onMounted(() => {
return item;
}),
gallery: gallery.value,
+ mainClass: $style.pswp,
children: '.image',
thumbSelector: '.image',
loop: false,
padding: window.innerWidth > 500 ? {
top: 32,
- bottom: 32,
+ bottom: 90,
left: 32,
right: 32,
} : {
top: 0,
- bottom: 0,
+ bottom: 78,
left: 0,
right: 0,
},
@@ -82,6 +86,7 @@ onMounted(() => {
const id = element.dataset.id;
const file = props.mediaList.find(media => media.id === id);
+ if (!file) return;
itemData.src = file.url;
itemData.w = Number(file.properties.width);
@@ -198,16 +203,14 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
overflow: hidden; // clipにするとバグる
border-radius: 8px;
}
-</style>
-<style lang="scss">
.pswp {
- // なぜか機能しない
- //z-index: v-bind(pswpZIndex);
- z-index: 2000000;
- --pswp-bg: var(--modalBg);
+ --pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
+ --pswp-bg: var(--modalBg) !important;
}
+</style>
+<style lang="scss">
.pswp__bg {
background: var(--modalBg);
backdrop-filter: var(--modalBgFilter);
@@ -219,7 +222,7 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
align-items: center;
position: absolute;
- bottom: 30px;
+ bottom: 20px;
left: 50%;
transform: translateX(-50%);
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 09d530c4ea..9e3022896c 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -36,7 +36,7 @@
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
- <span :class="$style.caret"><i class="ti ti-caret-right ti-fw"></i></span>
+ <span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
</button>
<button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 1040dac12e..bb1269562d 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -4,7 +4,7 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
- :class="$style.root"
+ :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
:tabindex="!isDeleted ? '-1' : undefined"
>
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
@@ -32,6 +32,7 @@
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
+ <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
</div>
</div>
<div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget">
@@ -76,14 +77,14 @@
</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" :max-number="16">
+ <template #more>
+ <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
+ {{ i18n.ts.more }}
+ </button>
+ </template>
+ </MkReactionsViewer>
<footer :class="$style.footer">
- <MkReactionsViewer :note="appearNote" :max-number="16">
- <template #more>
- <button class="_button" :class="$style.reactionDetailsButton" @click="showReactions">
- {{ i18n.ts.more }}
- </button>
- </template>
- </MkReactionsViewer>
<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>
@@ -156,6 +157,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { MenuItem } from '@/types/menu';
+import MkRippleEffect from '@/components/MkRippleEffect.vue';
const props = defineProps<{
note: misskey.entities.Note;
@@ -255,9 +257,19 @@ function renote(viaKeyboard = false) {
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
- os.apiWithDialog('notes/create', {
+ const el = renoteButton.value as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
+
+ os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
+ }).then(() => {
+ os.toast(i18n.ts.renoted);
});
},
}, {
@@ -276,8 +288,18 @@ function renote(viaKeyboard = false) {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
- os.apiWithDialog('notes/create', {
+ const el = renoteButton.value as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
+
+ os.api('notes/create', {
renoteId: appearNote.id,
+ }).then(() => {
+ os.toast(i18n.ts.renoted);
});
},
}, {
@@ -443,6 +465,34 @@ function showReactions(): void {
&:hover > .article > .main > .footer > .footerButton {
opacity: 1;
}
+
+ &.showActionsOnlyHover {
+ .footer {
+ visibility: hidden;
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ padding: 0 4px;
+ margin-bottom: 0 !important;
+ background: var(--popup);
+ border-radius: 8px;
+ box-shadow: 0px 4px 32px var(--shadow);
+ }
+
+ .footerButton {
+ font-size: 90%;
+
+ &:not(:last-child) {
+ margin-right: 0;
+ }
+ }
+ }
+
+ &.showActionsOnlyHover:hover {
+ .footer {
+ visibility: visible;
+ }
+ }
}
.tip {
@@ -541,14 +591,15 @@ function showReactions(): void {
}
.article {
+ position: relative;
display: flex;
- padding: 28px 32px 18px;
+ padding: 28px 32px;
}
.avatar {
flex-shrink: 0;
display: block !important;
- margin: 0 14px 8px 0;
+ margin: 0 14px 0 0;
width: 58px;
height: 58px;
position: sticky !important;
@@ -571,9 +622,9 @@ function showReactions(): void {
.showLess {
width: 100%;
- margin-top: 1em;
+ margin-top: 14px;
position: sticky;
- bottom: 1em;
+ bottom: calc(var(--stickyBottom, 0px) + 14px);
}
.showLessLabel {
@@ -653,6 +704,10 @@ function showReactions(): void {
font-size: 80%;
}
+.footer {
+ margin-bottom: -14px;
+}
+
.footerButton {
margin: 0;
padding: 8px;
@@ -683,7 +738,7 @@ function showReactions(): void {
}
.article {
- padding: 24px 26px 14px;
+ padding: 24px 26px;
}
.avatar {
@@ -702,7 +757,11 @@ function showReactions(): void {
}
.article {
- padding: 20px 22px 12px;
+ padding: 20px 22px;
+ }
+
+ .footer {
+ margin-bottom: -8px;
}
}
@@ -721,13 +780,13 @@ function showReactions(): void {
}
.article {
- padding: 14px 16px 9px;
+ padding: 14px 16px;
}
}
@container (max-width: 450px) {
.avatar {
- margin: 0 10px 8px 0;
+ margin: 0 10px 0 0;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
@@ -735,17 +794,21 @@ function showReactions(): void {
}
@container (max-width: 400px) {
- .footerButton {
- &:not(:last-child) {
- margin-right: 18px;
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 18px;
+ }
}
}
}
@container (max-width: 350px) {
- .footerButton {
- &:not(:last-child) {
- margin-right: 12px;
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 12px;
+ }
}
}
}
@@ -756,9 +819,11 @@ function showReactions(): void {
height: 44px;
}
- .footerButton {
- &:not(:last-child) {
- margin-right: 8px;
+ .root:not(.showActionsOnlyHover) {
+ .footerButton {
+ &:not(:last-child) {
+ margin-right: 8px;
+ }
}
}
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 2eebe999a5..f5f4a2afc1 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -161,6 +161,7 @@ import { deepClone } from '@/scripts/clone';
import { useTooltip } from '@/scripts/use-tooltip';
import { claimAchievement } from '@/scripts/achievements';
import { MenuItem } from '@/types/menu';
+import MkRippleEffect from '@/components/MkRippleEffect.vue';
const props = defineProps<{
note: misskey.entities.Note;
@@ -250,9 +251,19 @@ function renote(viaKeyboard = false) {
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
- os.apiWithDialog('notes/create', {
+ const el = renoteButton.value as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
+
+ os.api('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
+ }).then(() => {
+ os.toast(i18n.ts.renoted);
});
},
}, {
@@ -271,8 +282,18 @@ function renote(viaKeyboard = false) {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
- os.apiWithDialog('notes/create', {
+ const el = renoteButton.value as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
+
+ os.api('notes/create', {
renoteId: appearNote.id,
+ }).then(() => {
+ os.toast(i18n.ts.renoted);
});
},
}, {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index ffd9a20ef7..15d7ea2e14 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -18,6 +18,7 @@
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
</span>
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-world-off"></i></span>
+ <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
</div>
</header>
</template>
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index f73eab5b86..2b3e2c8646 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -437,8 +437,8 @@ function clear() {
}
function onKeydown(ev: KeyboardEvent) {
- if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post();
- if (ev.which === 27) emit('esc');
+ if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost) post();
+ if (ev.key === 'Escape') emit('esc');
}
function onCompositionUpdate(ev: CompositionEvent) {
@@ -489,9 +489,9 @@ function onDragover(ev) {
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
- case 'copy':
- case 'copyLink':
- case 'copyMove':
+ case 'copy':
+ case 'copyLink':
+ case 'copyMove':
ev.dataTransfer.dropEffect = 'copy';
break;
case 'linkMove':
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 81cbde0ac7..6f819bbbd7 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -34,7 +34,7 @@ export default defineComponent({
> button {
flex: 1;
padding: 10px 8px;
- border-radius: var(--radius);
+ border-radius: 999px;
&:disabled {
opacity: 1 !important;
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index 1aa48f88e6..6d59702569 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -53,7 +53,7 @@ onMounted(() => {
position: fixed;
left: 0;
right: 0;
- top: 0;
+ top: 50px;
margin: 0 auto;
margin-top: 16px;
min-width: 300px;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
new file mode 100644
index 0000000000..93e914f6dd
--- /dev/null
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -0,0 +1,222 @@
+<template>
+<Transition
+ :enter-active-class="$store.state.animation ? $style.transition_popup_enterActive : ''"
+ :leave-active-class="$store.state.animation ? $style.transition_popup_leaveActive : ''"
+ :enter-from-class="$store.state.animation ? $style.transition_popup_enterFrom : ''"
+ :leave-to-class="$store.state.animation ? $style.transition_popup_leaveTo : ''"
+ appear @after-leave="emit('closed')"
+>
+ <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
+ <div v-if="user != null">
+ <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
+ <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ $ts.followsYou }}</span>
+ </div>
+ <svg viewBox="0 0 128 128" :class="$style.avatarBack">
+ <g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
+ <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
+ </g>
+ </svg>
+ <MkAvatar :class="$style.avatar" :user="user" indicator/>
+ <div :class="$style.title">
+ <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
+ <div :class="$style.username"><MkAcct :user="user"/></div>
+ </div>
+ <div :class="$style.description">
+ <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
+ <div v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</div>
+ </div>
+ <div :class="$style.status">
+ <div :class="$style.statusItem">
+ <div :class="$style.statusItemLabel">{{ $ts.notes }}</div>
+ <div>{{ number(user.notesCount) }}</div>
+ </div>
+ <div :class="$style.statusItem">
+ <div :class="$style.statusItemLabel">{{ $ts.following }}</div>
+ <div>{{ number(user.followingCount) }}</div>
+ </div>
+ <div :class="$style.statusItem">
+ <div :class="$style.statusItemLabel">{{ $ts.followers }}</div>
+ <div>{{ number(user.followersCount) }}</div>
+ </div>
+ </div>
+ <button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button>
+ <MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/>
+ </div>
+ <div v-else>
+ <MkLoading/>
+ </div>
+ </div>
+</Transition>
+</template>
+
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import * as Acct from 'misskey-js/built/acct';
+import * as misskey from 'misskey-js';
+import MkFollowButton from '@/components/MkFollowButton.vue';
+import { userPage } from '@/filters/user';
+import * as os from '@/os';
+import { getUserMenu } from '@/scripts/get-user-menu';
+import number from '@/filters/number';
+import { i18n } from '@/i18n';
+
+const props = defineProps<{
+ showing: boolean;
+ q: string;
+ source: HTMLElement;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+ (ev: 'mouseover'): void;
+ (ev: 'mouseleave'): void;
+}>();
+
+const zIndex = os.claimZIndex('middle');
+let user = $ref<misskey.entities.UserDetailed | null>(null);
+let top = $ref(0);
+let left = $ref(0);
+
+function showMenu(ev: MouseEvent) {
+ os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
+}
+
+onMounted(() => {
+ if (typeof props.q === 'object') {
+ user = props.q;
+ } else {
+ const query = props.q.startsWith('@') ?
+ Acct.parse(props.q.substr(1)) :
+ { userId: props.q };
+
+ os.api('users/show', query).then(res => {
+ if (!props.showing) return;
+ user = res;
+ });
+ }
+
+ const rect = props.source.getBoundingClientRect();
+ const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
+ const y = rect.top + props.source.offsetHeight + window.pageYOffset;
+
+ top = y;
+ left = x;
+});
+</script>
+
+<style lang="scss" module>
+.transition_popup_enterActive,
+.transition_popup_leaveActive {
+ transition: opacity 0.15s, transform 0.15s !important;
+}
+.transition_popup_enterFrom,
+.transition_popup_leaveTo {
+ opacity: 0;
+ transform: scale(0.9);
+}
+
+.root {
+ position: absolute;
+ width: 300px;
+ overflow: clip;
+ transform-origin: center top;
+}
+
+.banner {
+ height: 78px;
+ background-color: rgba(0, 0, 0, 0.1);
+ background-size: cover;
+ background-position: center;
+}
+
+.followed {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ padding: 4px 8px;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.7);
+ font-size: 0.7em;
+ border-radius: 6px;
+}
+
+.avatarBack {
+ width: 100px;
+ position: absolute;
+ top: 28px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+}
+
+.avatar {
+ display: block;
+ position: absolute;
+ top: 38px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ z-index: 2;
+ width: 58px;
+ height: 58px;
+}
+
+.title {
+ position: relative;
+ z-index: 3;
+ display: block;
+ padding: 8px 26px 16px 26px;
+ margin-top: 16px;
+ text-align: center;
+}
+
+.name {
+ display: inline-block;
+ font-weight: bold;
+ word-break: break-all;
+}
+
+.username {
+ display: block;
+ font-size: 0.8em;
+ opacity: 0.7;
+}
+
+.description {
+ padding: 16px 26px;
+ font-size: 0.8em;
+ text-align: center;
+ border-top: solid 1px var(--divider);
+ border-bottom: solid 1px var(--divider);
+}
+
+.status {
+ padding: 16px 26px 16px 26px;
+}
+
+.statusItem {
+ display: inline-block;
+ width: 33%;
+ text-align: center;
+}
+
+.statusItemLabel {
+ font-size: 0.7em;
+ color: var(--fgTransparentWeak);
+}
+
+.menu {
+ position: absolute;
+ top: 8px;
+ right: 44px;
+ padding: 6px;
+ background: var(--panel);
+ border-radius: 999px;
+}
+
+.follow {
+ position: absolute !important;
+ top: 8px;
+ right: 8px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserPreview.vue b/packages/frontend/src/components/MkUserPreview.vue
deleted file mode 100644
index 1086a2c651..0000000000
--- a/packages/frontend/src/components/MkUserPreview.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<template>
-<Transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="emit('closed')">
- <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
- <div v-if="user != null" class="info">
- <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''">
- <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
- </div>
- <MkAvatar class="avatar" :user="user" indicator/>
- <div class="title">
- <MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
- <p class="username"><MkAcct :user="user"/></p>
- </div>
- <div class="description">
- <Mfm v-if="user.description" :text="user.description" :author="user" :i="$i"/>
- </div>
- <div class="status">
- <div>
- <p>{{ $ts.notes }}</p><span>{{ user.notesCount }}</span>
- </div>
- <div>
- <p>{{ $ts.following }}</p><span>{{ user.followingCount }}</span>
- </div>
- <div>
- <p>{{ $ts.followers }}</p><span>{{ user.followersCount }}</span>
- </div>
- </div>
- <button class="menu _button" @click="showMenu"><i class="ti ti-dots"></i></button>
- <MkFollowButton v-if="$i && user.id != $i.id" class="koudoku-button" :user="user" mini/>
- </div>
- <div v-else>
- <MkLoading/>
- </div>
- </div>
-</Transition>
-</template>
-
-<script lang="ts" setup>
-import { onMounted } from 'vue';
-import * as Acct from 'misskey-js/built/acct';
-import * as misskey from 'misskey-js';
-import MkFollowButton from '@/components/MkFollowButton.vue';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import { getUserMenu } from '@/scripts/get-user-menu';
-
-const props = defineProps<{
- showing: boolean;
- q: string;
- source: HTMLElement;
-}>();
-
-const emit = defineEmits<{
- (ev: 'closed'): void;
- (ev: 'mouseover'): void;
- (ev: 'mouseleave'): void;
-}>();
-
-const zIndex = os.claimZIndex('middle');
-let user = $ref<misskey.entities.UserDetailed | null>(null);
-let top = $ref(0);
-let left = $ref(0);
-
-function showMenu(ev: MouseEvent) {
- os.popupMenu(getUserMenu(user), ev.currentTarget ?? ev.target);
-}
-
-onMounted(() => {
- if (typeof props.q === 'object') {
- user = props.q;
- } else {
- const query = props.q.startsWith('@') ?
- Acct.parse(props.q.substr(1)) :
- { userId: props.q };
-
- os.api('users/show', query).then(res => {
- if (!props.showing) return;
- user = res;
- });
- }
-
- const rect = props.source.getBoundingClientRect();
- const x = ((rect.left + (props.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
- const y = rect.top + props.source.offsetHeight + window.pageYOffset;
-
- top = y;
- left = x;
-});
-</script>
-
-<style lang="scss" scoped>
-.popup-enter-active, .popup-leave-active {
- transition: opacity 0.15s, transform 0.15s !important;
-}
-.popup-enter-from, .popup-leave-to {
- opacity: 0;
- transform: scale(0.9);
-}
-
-.fxxzrfni {
- position: absolute;
- width: 300px;
- overflow: hidden;
- transform-origin: center top;
-
- > .info {
- > .banner {
- height: 84px;
- background-color: rgba(0, 0, 0, 0.1);
- background-size: cover;
- background-position: center;
- > .followed {
- position: absolute;
- top: 12px;
- left: 12px;
- padding: 4px 8px;
- color: #fff;
- background: rgba(0, 0, 0, 0.7);
- font-size: 0.7em;
- border-radius: 6px;
- }
- }
-
- > .avatar {
- display: block;
- position: absolute;
- top: 62px;
- left: 13px;
- z-index: 2;
- width: 58px;
- height: 58px;
- border: solid 3px var(--face);
- border-radius: 8px;
- }
-
- > .title {
- display: block;
- padding: 8px 0 8px 82px;
-
- > .name {
- display: inline-block;
- margin: 0;
- font-weight: bold;
- line-height: 16px;
- word-break: break-all;
- }
-
- > .username {
- display: block;
- margin: 0;
- line-height: 16px;
- font-size: 0.8em;
- color: var(--fg);
- opacity: 0.7;
- }
- }
-
- > .description {
- padding: 0 16px;
- font-size: 0.8em;
- color: var(--fg);
- }
-
- > .status {
- padding: 8px 16px;
-
- > div {
- display: inline-block;
- width: 33%;
-
- > p {
- margin: 0;
- font-size: 0.7em;
- color: var(--fg);
- }
-
- > span {
- font-size: 1em;
- color: var(--accent);
- }
- }
- }
-
- > .menu {
- position: absolute;
- top: 8px;
- right: 44px;
- padding: 6px;
- background: var(--panel);
- border-radius: 999px;
- }
-
- > .koudoku-button {
- position: absolute;
- top: 8px;
- right: 8px;
- }
- }
-}
-</style>
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 7e53507f2e..e7ad2b9a43 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -23,7 +23,7 @@
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
</span>
</div>
- <div :class="$style.content">
+ <div v-container :class="$style.content">
<slot></slot>
</div>
</div>
@@ -465,7 +465,7 @@ defineExpose({
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
//border-bottom: solid 1px var(--divider);
- font-size: 95%;
+ font-size: 90%;
font-weight: bold;
}
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 589ca92d75..4d968db6a3 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -96,7 +96,7 @@ function onTabClick(): void {
}
const calcBg = () => {
- const rawBg = metadata?.bg ?? 'var(--bg)';
+ const rawBg = 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index a3fee91a36..44c02088da 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -6,20 +6,19 @@
<div ref="bodyEl" :data-sticky-container-header-height="headerHeight">
<slot></slot>
</div>
+ <div ref="footerEl">
+ <slot name="footer"></slot>
+ </div>
</div>
</template>
-<script lang="ts">
-// なんか動かない
-//const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP');
-const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
-</script>
-
<script lang="ts" setup>
import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue';
+import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const';
const rootEl = $shallowRef<HTMLElement>();
const headerEl = $shallowRef<HTMLElement>();
+const footerEl = $shallowRef<HTMLElement>();
const bodyEl = $shallowRef<HTMLElement>();
let headerHeight = $ref<string | undefined>();
@@ -27,9 +26,23 @@ let childStickyTop = $ref(0);
const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
provide(CURRENT_STICKY_TOP, $$(childStickyTop));
+let footerHeight = $ref<string | undefined>();
+let childStickyBottom = $ref(0);
+const parentStickyBottom = inject<Ref<number>>(CURRENT_STICKY_BOTTOM, ref(0));
+provide(CURRENT_STICKY_BOTTOM, $$(childStickyBottom));
+
const calc = () => {
- childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
- headerHeight = headerEl.offsetHeight.toString();
+ // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
+ if (headerEl != null) {
+ childStickyTop = parentStickyTop.value + headerEl.offsetHeight;
+ headerHeight = headerEl.offsetHeight.toString();
+ }
+
+ // コンポーネントが表示されてないけどKeepAliveで残ってる場合などは null になる
+ if (footerEl != null) {
+ childStickyBottom = parentStickyBottom.value + footerEl.offsetHeight;
+ footerHeight = footerEl.offsetHeight.toString();
+ }
};
const observer = new ResizeObserver(() => {
@@ -41,7 +54,7 @@ const observer = new ResizeObserver(() => {
onMounted(() => {
calc();
- watch(parentStickyTop, calc);
+ watch([parentStickyTop, parentStickyBottom], calc);
watch($$(childStickyTop), () => {
bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`);
@@ -49,11 +62,22 @@ onMounted(() => {
immediate: true,
});
+ watch($$(childStickyBottom), () => {
+ bodyEl.style.setProperty('--stickyBottom', `${childStickyBottom}px`);
+ }, {
+ immediate: true,
+ });
+
headerEl.style.position = 'sticky';
headerEl.style.top = 'var(--stickyTop, 0)';
headerEl.style.zIndex = '1000';
+ footerEl.style.position = 'sticky';
+ footerEl.style.bottom = 'var(--stickyBottom, 0)';
+ footerEl.style.zIndex = '1000';
+
observer.observe(headerEl);
+ observer.observe(footerEl);
});
onUnmounted(() => {