From 9349f72227a96eb0e70404fa5bfea7044d3231a2 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sat, 11 Feb 2023 16:04:45 +0900 Subject: [PATCH 01/21] refactor(client): Refactor MkPageHeader #9869 (#9878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * disable animation * refactor(client): MkPageHeaderのタブをMkPageHeader.tabsに分離 animationをフォローするように * update CHANGELOG.md * remove unnecessary props --- CHANGELOG.md | 7 + .../components/global/MkPageHeader.tabs.vue | 218 ++++++++++++++++++ .../src/components/global/MkPageHeader.vue | 200 +--------------- packages/frontend/src/pages/timeline.vue | 7 +- 4 files changed, 239 insertions(+), 193 deletions(-) create mode 100644 packages/frontend/src/components/global/MkPageHeader.tabs.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b650e7de9..097195d531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ You should also include the user name that made the change. --> +## 13.x.x (unreleased) + +### Improvements +- アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 + +### Bugfixes +- ## 13.6.0 (2023/02/11) diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue new file mode 100644 index 0000000000..9b19c5dc87 --- /dev/null +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -0,0 +1,218 @@ +<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-if="t.key === tab" :class="$style.tabTitle">{{ 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 } 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); +}>(); + +let el = $shallowRef<HTMLElement | null>(null); +const tabRefs: Record<string, HTMLElement | null> = {}; +let 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 && 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 ro2: ResizeObserver | null; + +onMounted(() => { + watch([() => props.tab, () => props.tabs], () => { + nextTick(() => renderTab()); + }, { + immediate: true, + }); + + if (props.rootEl) { + ro2 = new ResizeObserver((entries, observer) => { + if (document.body.contains(el 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 { + 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: none; + pointer-events: none; + + &.animate { + transition: width 0.15s ease, left 0.15s ease; + } +} +</style> diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 23a39b9ac9..d39fcde1b5 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -19,27 +19,7 @@ </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"> <template v-for="action in actions"> @@ -48,34 +28,19 @@ </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> @@ -418,68 +302,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> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 31f4793dc4..a071361150 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -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, From 19c0027605dca3bb2af3a2943d7984165badc9c2 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sat, 11 Feb 2023 07:17:50 +0000 Subject: [PATCH 02/21] =?UTF-8?q?fix(client):=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A7=E3=82=A2?= =?UTF-8?q?=E3=82=AF=E3=83=86=E3=82=A3=E3=83=93=E3=83=86=E3=82=A3=E3=82=92?= =?UTF-8?q?=E8=A6=8B=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=84=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- packages/frontend/src/pages/user/index.vue | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 097195d531..c11d186926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ You should also include the user name that made the change. - アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 ### Bugfixes -- +- Client: ユーザーページでアクティビティを見ることができない問題を修正 ## 13.6.0 (2023/02/11) diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 84909f72c2..29aef21859 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -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); From 998c2b692a84829cd8cdee9265c7320f9555077e Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sat, 11 Feb 2023 11:35:28 +0000 Subject: [PATCH 03/21] :art: --- .../components/global/MkPageHeader.tabs.vue | 54 ++++++++++++------- .../src/components/global/MkPageHeader.vue | 31 ++++------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 9b19c5dc87..dae68c7e9c 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -10,7 +10,7 @@ :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> + <div v-show="t.key === tab" :class="[$style.tabTitle, $style.animate]">{{ t.title }}</div> </Transition> </div> </button> @@ -34,7 +34,7 @@ export type Tab = { </script> <script lang="ts" setup> -import { onMounted, onUnmounted, watch, nextTick } from 'vue'; +import { onMounted, onUnmounted, watch, nextTick, Transition, shallowRef } from 'vue'; import { defaultStore } from '@/store'; const props = withDefaults(defineProps<{ @@ -50,9 +50,9 @@ const emit = defineEmits<{ (ev: 'tabClick', key: string); }>(); -let el = $shallowRef<HTMLElement | null>(null); +const el = shallowRef<HTMLElement | null>(null); const tabRefs: Record<string, HTMLElement | null> = {}; -let tabHighlightEl = $shallowRef<HTMLElement | null>(null); +const tabHighlightEl = shallowRef<HTMLElement | null>(null); function onTabMousedown(tab: Tab, ev: MouseEvent): void { // ユーザビリティの観点からmousedown時にはonClickは呼ばない @@ -77,13 +77,13 @@ function onTabClick(t: Tab, ev: MouseEvent): void { function renderTab() { const tabEl = props.tab ? tabRefs[props.tab] : undefined; - if (tabEl && tabHighlightEl && tabHighlightEl.parentElement) { + 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.parentElement.getBoundingClientRect(); + const parentRect = tabHighlightEl.value.parentElement.getBoundingClientRect(); const rect = tabEl.getBoundingClientRect(); - tabHighlightEl.style.width = rect.width + 'px'; - tabHighlightEl.style.left = (rect.left - parentRect.left + tabHighlightEl.parentElement.scrollLeft) + 'px'; + tabHighlightEl.value.style.width = rect.width + 'px'; + tabHighlightEl.value.style.left = (rect.left - parentRect.left + tabHighlightEl.value.parentElement.scrollLeft) + 'px'; } } @@ -99,22 +99,32 @@ function onTabWheel(ev: WheelEvent) { return false; } -function enter(el: HTMLElement) { +let entering = false; + +async function enter(el: HTMLElement) { + entering = true; const elementWidth = el.getBoundingClientRect().width; el.style.width = '0'; - el.offsetWidth; // reflow + el.style.paddingLeft = '0'; + el.offsetWidth; // force reflow el.style.width = elementWidth + 'px'; - setTimeout(renderTab, 70); + el.style.paddingLeft = ''; + nextTick(() => { + entering = false; + }); + + setTimeout(renderTab, 170); } function afterEnter(el: HTMLElement) { - el.style.width = ''; - nextTick(renderTab); + //el.style.width = ''; } -function leave(el: HTMLElement) { +async function leave(el: HTMLElement) { const elementWidth = el.getBoundingClientRect().width; el.style.width = elementWidth + 'px'; - el.offsetWidth; // reflow + el.style.paddingLeft = ''; + el.offsetWidth; // force reflow el.style.width = '0'; + el.style.paddingLeft = '0'; } function afterLeave(el: HTMLElement) { el.style.width = ''; @@ -124,14 +134,17 @@ let ro2: ResizeObserver | null; onMounted(() => { watch([() => props.tab, () => props.tabs], () => { - nextTick(() => renderTab()); + nextTick(() => { + if (entering) return; + renderTab(); + }); }, { immediate: true, }); if (props.rootEl) { ro2 = new ResizeObserver((entries, observer) => { - if (document.body.contains(el as HTMLElement)) { + if (document.body.contains(el.value as HTMLElement)) { nextTick(() => renderTab()); } }); @@ -194,12 +207,15 @@ onUnmounted(() => { } .tabIcon+.tabTitle { - margin-left: 8px; + padding-left: 8px; } .tabTitle { overflow: hidden; - transition: width 0.15s ease-in-out; + + &.animate { + transition: width .15s linear, padding-left .15s linear; + } } .tabHighlight { diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index d39fcde1b5..6c908d07b1 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -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"> @@ -21,7 +21,7 @@ </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> @@ -142,6 +142,7 @@ onUnmounted(() => { .upper { --height: 50px; display: flex; + gap: var(--margin); height: var(--height); .tabs:first-child { @@ -151,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; @@ -170,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; - } } } } @@ -198,8 +191,6 @@ onUnmounted(() => { align-items: center; min-width: var(--height); height: var(--height); - margin: 0 var(--margin); - &:empty { width: var(--height); } @@ -207,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 { From 5d02405a98a36dc726e1b6db9973d957a8ef6464 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sat, 11 Feb 2023 13:06:54 +0000 Subject: [PATCH 04/21] :art: --- packages/frontend/src/components/global/MkPageHeader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 6c908d07b1..803efb1690 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -248,7 +248,7 @@ onUnmounted(() => { white-space: nowrap; text-align: left; font-weight: bold; - flex-shrink: 0; + flex-shrink: 1; margin-left: 24px; } From e1d41063cd1783c87b95d1374e24368a6451b1e4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 08:18:22 +0900 Subject: [PATCH 05/21] enhance(client): make renote collapsing optional Resolve #9891 --- locales/ja-JP.yml | 1 + packages/frontend/src/components/MkNote.vue | 4 ++-- packages/frontend/src/pages/settings/general.vue | 2 ++ packages/frontend/src/store.ts | 4 ++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5c919c3032..4ef9fd5aec 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -951,6 +951,7 @@ thisPostMayBeAnnoying: "この投稿は迷惑になる可能性があります thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" +collapseRenotes: "見たことのあるRenoteを省略して表示" _achievements: earnedAt: "獲得日時" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e910fbab01..7d02dadf4e 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -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> @@ -206,7 +206,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); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index b4851df176..1b492b15cf 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -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')); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 80bd22a813..46e55900cd 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -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, From ef860a8f844977aea9043f83dbbf761c3da70727 Mon Sep 17 00:00:00 2001 From: RyotaK <49341894+Ry0taK@users.noreply.github.com> Date: Sun, 12 Feb 2023 08:21:13 +0900 Subject: [PATCH 06/21] =?UTF-8?q?CLI=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB?= =?UTF-8?q?=E3=81=8A=E3=81=84=E3=81=A6API=E3=83=AA=E3=82=AF=E3=82=A8?= =?UTF-8?q?=E3=82=B9=E3=83=88=E6=99=82=E3=81=ABContent-Type=E3=83=98?= =?UTF-8?q?=E3=83=83=E3=83=80=E3=82=92=E4=BB=98=E4=B8=8E=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4=20(#9887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/web/cli.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 3dff1d4860..3467f7ac2a 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -11,6 +11,9 @@ window.onload = async () => { // Send request fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { + headers: { + 'Content-Type': 'application/json' + }, method: 'POST', body: JSON.stringify(data), credentials: 'omit', From 1ac7c154d7922d59e255b2d6c5c4c27b1f1b242a Mon Sep 17 00:00:00 2001 From: futchitwo <74236683+futchitwo@users.noreply.github.com> Date: Sun, 12 Feb 2023 08:21:40 +0900 Subject: [PATCH 07/21] fix: pagenation (#9885) --- packages/frontend/src/components/MkPagination.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 04c8616c9a..224a42cdc2 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -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"/> From f28aea9e303af96978da5e27739b5fd8e23b8f86 Mon Sep 17 00:00:00 2001 From: momoirodouhu <momoirodouhu@gmail.com> Date: Sun, 12 Feb 2023 08:22:42 +0900 Subject: [PATCH 08/21] add cors header to ActivityPubServerService.ts (#9888) * add cors header to ActivityPubServerService.ts * Update CHANGELOG.md --- CHANGELOG.md | 1 + packages/backend/src/server/ActivityPubServerService.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11d186926..7d0fbd0a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ You should also include the user name that made the change. ### Improvements - アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 +- Backend: activitypub情報がcorsでブロックされないようヘッダーを追加 ### Bugfixes - Client: ユーザーページでアクティビティを見ることができない問題を修正 diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 186d3822d8..5480395eeb 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -441,6 +441,14 @@ export class ActivityPubServerService { fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')); + fastify.addHook('onRequest', (request, reply, done) => { + reply.header('Access-Control-Allow-Headers', 'Accept'); + reply.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); + reply.header('Access-Control-Allow-Origin', '*'); + reply.header('Access-Control-Expose-Headers', 'Vary'); + done(); + }); + //#region Routing // inbox (limit: 64kb) fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply)); From ac7e2ecb59e2ad8c29bca52fca4e4d9b316403c5 Mon Sep 17 00:00:00 2001 From: KOKO <taitokokoa+rassi@gmail.com> Date: Sun, 12 Feb 2023 08:23:14 +0900 Subject: [PATCH 09/21] =?UTF-8?q?fix:=20=E5=BA=83=E5=91=8A=E3=81=AEexpires?= =?UTF-8?q?At=E3=82=92LocalTZ=E5=88=86=E3=81=9A=E3=82=89=E3=81=97=E3=81=A6?= =?UTF-8?q?=E5=88=9D=E6=9C=9F=E5=8C=96=20(#9876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 広告のexpiresAtをLocalTZ分ずらして初期化 * chore: 不要なインポートを削除 --- packages/frontend/src/pages/admin/ads.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 4d6f32f9a9..701ec31b65 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -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), }; }); }); From 3c7e1ff92ef4100347ee2151c3edfc431853532b Mon Sep 17 00:00:00 2001 From: RyotaK <49341894+Ry0taK@users.noreply.github.com> Date: Sun, 12 Feb 2023 09:07:56 +0900 Subject: [PATCH 10/21] =?UTF-8?q?Dev=20Container=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0=20(#9872)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dev Containerの設定を追加 * テンプレート生成時に含まれていたコメントを削除 * 起動スクリプトを分割 JSONの中にベタ書きすると長くなるので * 改行 * Dev Containerの使用方法を追記 --- .devcontainer/Dockerfile | 1 + .devcontainer/devcontainer.json | 11 +++ .devcontainer/devcontainer.yml | 146 +++++++++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 52 +++++++++++ .devcontainer/init.sh | 9 ++ .gitignore | 1 + CONTRIBUTING.md | 19 ++++ 7 files changed, 239 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/devcontainer.yml create mode 100644 .devcontainer/docker-compose.yml create mode 100755 .devcontainer/init.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..b6ebcf6ad3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:0-18 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e92f9dff78 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "name": "Misskey", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers-contrib/features/pnpm:2": {} + }, + "forwardPorts": [3000], + "postCreateCommand": ".devcontainer/init.sh" +} diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml new file mode 100644 index 0000000000..8a363a15dc --- /dev/null +++ b/.devcontainer/devcontainer.yml @@ -0,0 +1,146 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: http://127.0.0.1:3000/ + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: postgres + pass: postgres + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +allowedPrivateNetworks: [ + '127.0.0.1/32' +] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..6cb21844ac --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../..:/workspaces:cached + + command: sleep infinity + + networks: + - internal_network + - external_network + + redis: + restart: always + image: redis:7-alpine + networks: + - internal_network + volumes: + - ../redis:/data + healthcheck: + test: "redis-cli ping" + interval: 5s + retries: 20 + + db: + restart: unless-stopped + image: postgres:15-alpine + networks: + - internal_network + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: misskey + volumes: + - ../db:/var/lib/postgresql/data + healthcheck: + test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" + interval: 5s + retries: 20 + +volumes: + postgres-data: + +networks: + internal_network: + internal: true + external_network: diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh new file mode 100755 index 0000000000..552b229fa5 --- /dev/null +++ b/.devcontainer/init.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -xe + +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate diff --git a/.gitignore b/.gitignore index f532cdaa7e..62b818c629 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ coverage !/.config/docker_example.yml !/.config/docker_example.env docker-compose.yml +!/.devcontainer/docker-compose.yml # misskey /build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e539926789..de0a1abb45 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,6 +111,25 @@ command. - Vite HMR (just the `vite` command) is available. The behavior may be different from production. - Service Worker is watched by esbuild. +### Dev Container +Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. + +It will run the following command automatically inside the container. +``` bash +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate +``` + +After finishing the migration, run the `pnpm dev` command to start the development server. + +``` bash +pnpm dev +``` + ## Testing - Test codes are located in [`/packages/backend/test`](/packages/backend/test). From ee03ab8d2c06254480e8b78d3c4eab4a78409ad5 Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sun, 12 Feb 2023 09:13:47 +0900 Subject: [PATCH 11/21] enhance(server): videoThumbnailGenerator config (#9845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(server): videoThumbnailGenerator config * :v: * fix * 相対url * サムネイルのproxyRemoteFilesは直接プロキシを指定する * メディアプロキシ --- .config/example.yml | 9 ++++ packages/backend/src/config.ts | 6 +++ packages/backend/src/core/DriveService.ts | 8 +++ .../src/core/VideoProcessingService.ts | 14 +++++ .../core/entities/DriveFileEntityService.ts | 53 ++++++++++++++----- .../backend/src/server/FileServerService.ts | 6 +++ 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/.config/example.yml b/.config/example.yml index a19b5d04e8..92b8726623 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -131,11 +131,20 @@ proxyBypassHosts: # Media Proxy # Reference Implementation: https://github.com/misskey-dev/media-proxy +# * Deliver a common cache between instances +# * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy # Proxy remote files (default: false) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. #proxyRemoteFiles: true +# Movie Thumbnail Generation URL +# There is no reference implementation. +# For example, Misskey will point to the following URL: +# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 +#videoThumbnailGenerator: https://example.com + # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index aa98ef1d22..73f45e92e1 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -67,6 +67,7 @@ export type Source = { mediaProxy?: string; proxyRemoteFiles?: boolean; + videoThumbnailGenerator?: string; signToActivityPubGet?: boolean; }; @@ -89,6 +90,7 @@ export type Mixin = { clientManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; + videoThumbnailGenerator: string | null; }; export type Config = Source & Mixin; @@ -144,6 +146,10 @@ export function loadConfig() { mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy; mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy; + mixin.videoThumbnailGenerator = config.videoThumbnailGenerator ? + config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator + : null; + if (!config.redis.prefix) config.redis.prefix = mixin.host; return Object.assign(config, mixin); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 598a457e83..42a430ea75 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -250,6 +250,14 @@ export class DriveService { @bindThis public async generateAlts(path: string, type: string, generateWeb: boolean) { if (type.startsWith('video/')) { + if (this.config.videoThumbnailGenerator != null) { + // videoThumbnailGeneratorが指定されていたら動画サムネイル生成はスキップ + return { + webpublic: null, + thumbnail: null, + } + } + try { const thumbnail = await this.videoProcessingService.generateVideoThumbnail(path); return { diff --git a/packages/backend/src/core/VideoProcessingService.ts b/packages/backend/src/core/VideoProcessingService.ts index ea5701decc..dd6c51c217 100644 --- a/packages/backend/src/core/VideoProcessingService.ts +++ b/packages/backend/src/core/VideoProcessingService.ts @@ -6,6 +6,7 @@ import { ImageProcessingService } from '@/core/ImageProcessingService.js'; import type { IImage } from '@/core/ImageProcessingService.js'; import { createTempDir } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; +import { appendQuery, query } from '@/misc/prelude/url.js'; @Injectable() export class VideoProcessingService { @@ -41,5 +42,18 @@ export class VideoProcessingService { cleanup(); } } + + @bindThis + public getExternalVideoThumbnailUrl(url: string): string | null { + if (this.config.videoThumbnailGenerator == null) return null; + + return appendQuery( + `${this.config.videoThumbnailGenerator}/thumbnail.webp`, + query({ + thumbnail: '1', + url, + }) + ) + } } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 9dd115d45a..b8550cd73e 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -13,6 +13,7 @@ import { deepClone } from '@/misc/clone.js'; import { UtilityService } from '../UtilityService.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFolderEntityService } from './DriveFolderEntityService.js'; +import { VideoProcessingService } from '../VideoProcessingService.js'; type PackOptions = { detail?: boolean, @@ -43,6 +44,7 @@ export class DriveFileEntityService { private utilityService: UtilityService, private driveFolderEntityService: DriveFolderEntityService, + private videoProcessingService: VideoProcessingService, ) { } @@ -72,40 +74,63 @@ export class DriveFileEntityService { } @bindThis - public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail - const proxiedUrl = (url: string) => appendQuery( + private getProxiedUrl(url: string, mode?: 'static' | 'avatar'): string | null { + return appendQuery( `${this.config.mediaProxy}/${mode ?? 'image'}.webp`, query({ url, ...(mode ? { [mode]: '1' } : {}), }) - ); + ) + } + @bindThis + public getThumbnailUrl(file: DriveFile): string | null { + if (file.type.startsWith('video')) { + if (file.thumbnailUrl) return file.thumbnailUrl; + + if (this.config.videoThumbnailGenerator == null) { + return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri); + } + } else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { + // 動画ではなくリモートかつメディアプロキシ + return this.getProxiedUrl(file.uri, 'static'); + } + + if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + // リモートかつ期限切れはローカルプロキシを試みる + // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 + // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する + return this.getProxiedUrl(file.uri, 'static'); + } + + const url = file.webpublicUrl ?? file.url; + + return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? this.getProxiedUrl(url, 'static') : null); + } + + @bindThis + public getPublicUrl(file: DriveFile, mode?: 'avatar'): string | null { // static = thumbnail // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) { - if (!(mode === 'static' && file.type.startsWith('video'))) { - return proxiedUrl(file.uri); - } + return this.getProxiedUrl(file.uri, mode); } // リモートかつ期限切れはローカルプロキシを試みる if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { - const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey; + const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 const url = `${this.config.url}/files/${key}`; - if (mode === 'avatar') return proxiedUrl(file.uri); + if (mode === 'avatar') return this.getProxiedUrl(file.uri, 'avatar'); return url; } } const url = file.webpublicUrl ?? file.url; - if (mode === 'static') { - return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null); - } if (mode === 'avatar') { - return proxiedUrl(url); + return this.getProxiedUrl(url, 'avatar'); } return url; } @@ -183,7 +208,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { @@ -218,7 +243,7 @@ export class DriveFileEntityService { blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), - thumbnailUrl: this.getPublicUrl(file, 'static'), + thumbnailUrl: this.getThumbnailUrl(file), comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, { diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 49ded6c28e..f4bc568fdc 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -150,6 +150,12 @@ export class FileServerService { file.cleanup(); return await reply.redirect(301, url.toString()); } else if (file.mime.startsWith('video/')) { + const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); + if (externalThumbnail) { + file.cleanup(); + return await reply.redirect(301, externalThumbnail); + } + image = await this.videoProcessingService.generateVideoThumbnail(file.path); } } From f5bfc6f0c167737482d9ff9a6fe144024c76849f Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 10:21:17 +0900 Subject: [PATCH 12/21] enhance(client): improve api error handling --- locales/ja-JP.yml | 3 +++ packages/frontend/src/os.ts | 27 +++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4ef9fd5aec..14693439b6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -952,6 +952,9 @@ thisPostMayBeAnnoyingHome: "ホームに投稿" thisPostMayBeAnnoyingCancel: "やめる" thisPostMayBeAnnoyingIgnore: "このまま投稿" collapseRenotes: "見たことのあるRenoteを省略して表示" +internalServerError: "サーバー内部エラー" +internalServerErrorDescription: "サーバー内部で予期しないエラーが発生しました。" +copyErrorInfo: "エラー情報をコピー" _achievements: earnedAt: "獲得日時" diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 639f4eaf17..6bff12661f 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -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')) { From 9ddf62d8b7b955217108a17e9074d50b5722dd3a Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 10:26:27 +0900 Subject: [PATCH 13/21] =?UTF-8?q?enhance:=20=E3=83=AC=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=83=9F=E3=83=83=E3=83=88=E3=82=920%=E3=81=AB?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../backend/src/server/api/ApiCallService.ts | 18 ++++++++++-------- .../frontend/src/pages/admin/roles.editor.vue | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0fbd0a23..2ce5292040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ You should also include the user name that made the change. ### Improvements - アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 - Backend: activitypub情報がcorsでブロックされないようヘッダーを追加 +- enhance: レートリミットを0%にできるように ### Bugfixes - Client: ユーザーページでアクティビティを見ることができない問題を修正 diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 395a1c468a..2f3e7a44a9 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -227,15 +227,17 @@ export class ApiCallService implements OnApplicationShutdown { // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; - // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, + if (factor > 0) { + // Rate limit + await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }); }); - }); + } } if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 086537a94a..d89a68f982 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -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> From b55d26387b6a56c90c9dc96a90d9fffaef98bab4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 10:36:43 +0900 Subject: [PATCH 14/21] improve error handling --- packages/backend/src/core/UserListService.ts | 4 +++- .../server/api/endpoints/users/lists/push.ts | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index c174394999..bc726a1feb 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -14,6 +14,8 @@ import { RoleService } from '@/core/RoleService.js'; @Injectable() export class UserListService { + public static TooManyUsersError = class extends Error {}; + constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -36,7 +38,7 @@ export class UserListService { userListId: list.id, }); if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { - throw new Error('Too many users'); + throw new UserListService.TooManyUsersError(); } await this.userListJoiningsRepository.insert({ diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 3a079ee1ab..1c1fdc23f1 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -45,6 +45,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b', }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '2dd9752e-a338-413d-8eec-41814430989b', + }, }, } as const; @@ -110,8 +116,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { throw new ApiError(meta.errors.alreadyAdded); } - // Push the user - await this.userListService.push(user, userList, me); + try { + await this.userListService.push(user, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + + throw err; + } }); } } From 784fc7b3f58a5b4b93c67270a3c3e8acb467771b Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 10:46:14 +0900 Subject: [PATCH 15/21] :art: --- packages/frontend/src/components/MkNote.vue | 6 ++++++ packages/frontend/src/components/MkNoteSimple.vue | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 7d02dadf4e..1bad32c4ac 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -704,6 +704,12 @@ function showReactions(): void { } } +@container (max-width: 250px) { + .quoteNote { + padding: 12px; + } +} + .muted { padding: 8px; text-align: center; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 757b325a06..b38a4afa8b 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -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; From 2f48d109ddfc6a9f1e038b973d7011ee11bb4b16 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 10:59:22 +0900 Subject: [PATCH 16/21] enhance(client): make possible to in-channel renote/quote --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 ++ packages/frontend/src/components/MkNote.vue | 31 +++++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce5292040..a9078520fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ You should also include the user name that made the change. - アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 - Backend: activitypub情報がcorsでブロックされないようヘッダーを追加 - enhance: レートリミットを0%にできるように +- チャンネル内Renoteを行えるように ### Bugfixes - Client: ユーザーページでアクティビティを見ることができない問題を修正 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 14693439b6..99ce82ce4c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -103,6 +103,8 @@ renoted: "Renoteしました。" cantRenote: "この投稿はRenoteできません。" cantReRenote: "RenoteをRenoteすることはできません。" quote: "引用" +inChannelRenote: "チャンネル内Renote" +inChannelQuote: "チャンネル内引用" pinnedNote: "ピン留めされたノート" pinned: "ピン留め" you: "あなた" diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 1bad32c4ac..f50a42ebaf 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -247,7 +247,32 @@ useTooltip(renoteButton, async (showing) => { function renote(viaKeyboard = false) { pleaseLogin(); - os.popupMenu([{ + + let items = []; + + 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 +288,9 @@ function renote(viaKeyboard = false) { renote: appearNote, }); }, - }], renoteButton.value, { + }]); + + os.popupMenu(items, renoteButton.value, { viaKeyboard, }); } From ee5b417354bda3f1ae6ca09a31aed2c78c49c524 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 11:02:11 +0900 Subject: [PATCH 17/21] tweak --- packages/frontend/src/components/MkNote.vue | 3 +- .../src/components/MkNoteDetailed.vue | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index f50a42ebaf..c9c512c36e 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -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; @@ -248,7 +249,7 @@ useTooltip(renoteButton, async (showing) => { function renote(viaKeyboard = false) { pleaseLogin(); - let items = []; + let items = [] as MenuItem[]; if (appearNote.channel) { items = items.concat([{ diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 0da06c4f14..92bdadc562 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -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, }); } From 56b23a64a3d01b16fe37ead2fd4ee7a4f99c066c Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 11:06:26 +0900 Subject: [PATCH 18/21] clean up --- packages/frontend/src/pages/user/home.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index fab47d09e2..8948e85ea4 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -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> From e98740c28516414fc83d6939c68a8c72958fd861 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 11:10:37 +0900 Subject: [PATCH 19/21] =?UTF-8?q?enhance(client):=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AE=E3=83=9B=E3=83=BC=E3=83=A0=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AB=E3=82=82TL=E3=82=92=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/user/home.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 8948e85ea4..6d942d4391 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -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 { From c75fc266e9b3704d92e462660761baba980188cd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 11:19:39 +0900 Subject: [PATCH 20/21] New Crowdin updates (#9880) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (English) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Japanese, Kansai) --- locales/en-US.yml | 6 ++++++ locales/ja-KS.yml | 2 ++ locales/ko-KR.yml | 19 +++++++++++++++++++ locales/uk-UA.yml | 7 +++++-- locales/zh-CN.yml | 6 ++++++ locales/zh-TW.yml | 7 +++++++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index b18e57af8e..beb5242b1c 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -467,6 +467,8 @@ youHaveNoGroups: "You have no groups" joinOrCreateGroup: "Get invited to a group or create your own." noHistory: "No history available" signinHistory: "Login history" +enableAdvancedMfm: "Enable advanced MFM" +enableAnimatedMfm: "Enable MFM with animation" doing: "Processing..." category: "Category" tags: "Tags" @@ -945,6 +947,10 @@ selectFromPresets: "Choose from presets" achievements: "Achievements" gotInvalidResponseError: "Invalid server response" gotInvalidResponseErrorDescription: "The server may be unreachable or undergoing maintenance. Please try again later." +thisPostMayBeAnnoying: "This note may annoy others." +thisPostMayBeAnnoyingHome: "Post to home timeline" +thisPostMayBeAnnoyingCancel: "Cancel" +thisPostMayBeAnnoyingIgnore: "Post anyway" _achievements: earnedAt: "Unlocked at" _types: diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index ced47e1175..cbbd928b2c 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -947,6 +947,8 @@ selectFromPresets: "プリセットから選ぶ" achievements: "実績" gotInvalidResponseError: "サーバー黙っとるわ、知らんけど" gotInvalidResponseErrorDescription: "サーバーいま日曜日。またきて月曜日。" +thisPostMayBeAnnoying: "この投稿は迷惑かもしらんで。" +collapseRenotes: "見たことあるRenoteは省略やで" _achievements: earnedAt: "貰った日ぃ" _types: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 2def444095..5096a5d316 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -129,6 +129,7 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?" unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" selectList: "리스트 선택" +selectChannel: "채널 선택" selectAntenna: "안테나 선택" selectWidget: "위젯 선택" editWidgets: "위젯 편집" @@ -256,6 +257,8 @@ noMoreHistory: "이것보다 과거의 기록이 없습니다" startMessaging: "대화 시작하기" nUsersRead: "{n}명이 읽음" agreeTo: "{0}에 동의" +agreeBelow: "아래 내용에 동의합니다" +basicNotesBeforeCreateAccount: "기본적인 주의사항" tos: "이용 약관" start: "시작하기" home: "홈" @@ -464,6 +467,8 @@ youHaveNoGroups: "그룹이 없습니다" joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들어 보세요." noHistory: "기록이 없습니다" signinHistory: "로그인 기록" +enableAdvancedMfm: "고급 MFM을 활성화" +enableAnimatedMfm: "움직임이 있는 MFM을 활성화" doing: "잠시만요" category: "카테고리" tags: "태그" @@ -860,6 +865,8 @@ failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" cropImage: "이미지 자르기" cropImageAsk: "이미지를 자르시겠습니까?" +cropYes: "잘라내기" +cropNo: "그대로 사용" file: "파일" recentNHours: "최근 {n}시간" recentNDays: "최근 {n}일" @@ -938,6 +945,12 @@ cannotPerformTemporaryDescription: "조작 횟수 제한을 초과하여 일시 preset: "프리셋" selectFromPresets: "프리셋에서 선택" achievements: "도전 과제" +gotInvalidResponseError: "서버의 응답이 올바르지 않습니다" +gotInvalidResponseErrorDescription: " 서버가 다운되었거나 점검중일 가능성이 있습니다. 잠시후에 다시 시도해 주십시오." +thisPostMayBeAnnoying: "이 게시물은 다른 유저에게 피해를 줄 가능성이 있습니다." +thisPostMayBeAnnoyingHome: "홈에 게시" +thisPostMayBeAnnoyingCancel: "그만두기" +thisPostMayBeAnnoyingIgnore: "이대로 게시" _achievements: earnedAt: "달성 일시" _types: @@ -1194,6 +1207,9 @@ _role: baseRole: "기본 역할" useBaseValue: "기본값 사용" chooseRoleToAssign: "할당할 역할 선택" + iconUrl: "아이콘 URL" + asBadge: "뱃지로 표시" + descriptionOfAsBadge: "활성화하면 유저명 옆에 역할의 아이콘이 표시됩니다." canEditMembersByModerator: "모더레이터의 역할 수정 허용" descriptionOfCanEditMembersByModerator: "이 옵션을 켜면 모더레이터도 이 역할에 사용자를 할당하거나 삭제할 수 있습니다. 꺼져 있으면 관리자만 할당이 가능합니다." priority: "우선순위" @@ -1523,12 +1539,15 @@ _permissions: "read:gallery-likes": "갤러리의 좋아요를 확인합니다" "write:gallery-likes": "갤러리에 좋아요를 추가하거나 취소합니다" _auth: + shareAccessTitle: "어플리케이션의 접근 허가" shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?" shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?" + permission: "{name}에서 다음 권한을 요청하였습니다" permissionAsk: "이 앱은 다음의 권한을 요청합니다" pleaseGoBack: "앱으로 돌아가서 시도해 주세요" callback: "앱으로 돌아갑니다" denied: "접근이 거부되었습니다" + pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오." _antennaSources: all: "모든 노트" homeTimeline: "팔로우중인 유저의 노트" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 2305fcab1b..27882e50f6 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -166,7 +166,7 @@ recipient: "Отримувач" annotation: "Коментарі" federation: "Федіверс" instances: "Інстанс" -registeredAt: "Приєднався(лась)" +registeredAt: "Реєстрація" latestRequestReceivedAt: "Останній запит прийнято" latestStatus: "Останній статус" storageUsage: "Використання простору" @@ -263,7 +263,7 @@ activity: "Активність" images: "Зображення" birthday: "День народження" yearsOld: "{age} років" -registeredDate: "Приєднався(лась)" +registeredDate: "Приєднання" location: "Локація" theme: "Тема" themeForLightMode: "Світла тема" @@ -1086,6 +1086,9 @@ _achievements: _outputHelloWorldOnScratchpad: title: "Hello, world!" description: "Вивести \"hello world\" у Скретчпаді" + _reactWithoutRead: + title: "Прочитали як слід?" + description: "Реакція на нотатку, що містить понад 100 символів, протягом 3 секунд після її публікації" _clickedClickHere: title: "Натисніть тут" description: "Натиснуто тут" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 16b824b975..71ca55d9e5 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -467,6 +467,8 @@ youHaveNoGroups: "没有群组" joinOrCreateGroup: "请加入一个现有的群组,或者创建新群组。" noHistory: "没有历史记录" signinHistory: "登录历史" +enableAdvancedMfm: "启用扩展MFM" +enableAnimatedMfm: "启用MFM动画" doing: "正在进行" category: "类别" tags: "标签" @@ -945,6 +947,10 @@ selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "服务器无应答" gotInvalidResponseErrorDescription: "您的网络连接可能出现了问题, 或是远程服务器暂时不可用. 请稍后重试。" +thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。" +thisPostMayBeAnnoyingHome: "发到首页" +thisPostMayBeAnnoyingCancel: "取消" +thisPostMayBeAnnoyingIgnore: "就这样发布" _achievements: earnedAt: "达成时间" _types: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index ac0e38cf0c..f99ca91e63 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -467,6 +467,8 @@ youHaveNoGroups: "找不到群組" joinOrCreateGroup: "請加入現有群組,或創建新群組。" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" +enableAdvancedMfm: "啟用高級MFM" +enableAnimatedMfm: "啟用MFM動畫" doing: "正在進行" category: "類別" tags: "標籤" @@ -945,6 +947,11 @@ selectFromPresets: "從預設值中選擇" achievements: "成就" gotInvalidResponseError: "伺服器的回應無效" gotInvalidResponseErrorDescription: "伺服器可能已關閉或者在維護中,請稍後再試。" +thisPostMayBeAnnoying: "這篇貼文可能會造成別人的困擾。" +thisPostMayBeAnnoyingHome: "發布到首頁" +thisPostMayBeAnnoyingCancel: "退出" +thisPostMayBeAnnoyingIgnore: "直接發布貼文" +collapseRenotes: "省略顯示已看過的轉發貼文" _achievements: earnedAt: "獲得日期" _types: From b427bf70a8519e2a54aa6231d05b58f00542542c Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 12 Feb 2023 11:20:08 +0900 Subject: [PATCH 21/21] 13.6.1 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9078520fd..3b65932162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ You should also include the user name that made the change. --> -## 13.x.x (unreleased) +## 13.6.1 (2023/02/12) ### Improvements - アニメーションを少なくする設定の時、MkPageHeaderのタブアニメーションを無効化 diff --git a/package.json b/package.json index 7cc3f6cf5c..5a6b807d66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.6.0", + "version": "13.6.1", "codename": "nasubi", "repository": { "type": "git",