From b6cc8c46c70ec547c15be95c3e63f7c3927aa0b1 Mon Sep 17 00:00:00 2001 From: uni-kakurenbo Date: Tue, 28 Oct 2025 14:39:56 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E5=8F=82=E7=85=A7=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E3=81=AB=E3=82=88=E3=81=A3=E3=81=A6?= =?UTF-8?q?=20`effectScope`=20=E3=82=92=E8=87=AA=E5=8B=95=E3=81=A7=20?= =?UTF-8?q?=E4=BD=9C=E6=88=90/=E7=A0=B4=E6=A3=84=20=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/createSharedComposable.ts | 33 +++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/lib/utils/createSharedComposable.ts diff --git a/src/lib/utils/createSharedComposable.ts b/src/lib/utils/createSharedComposable.ts new file mode 100644 index 0000000000..a8d4856e10 --- /dev/null +++ b/src/lib/utils/createSharedComposable.ts @@ -0,0 +1,33 @@ +import { effectScope, onScopeDispose, type EffectScope } from 'vue' +import type { Invocable } from '/@/types/utility' + +const createSharedComposable = (fn: Fn) => { + let subscribers = 0 + let state: ReturnType | null = null + let scope: EffectScope | null = null + + const dispose = () => { + if (--subscribers <= 0) { + scope?.stop() + state = scope = null + } + } + + const wrapped = (...args: Parameters) => { + ++subscribers + + if (!state) { + scope = effectScope(true) + state = scope.run(() => fn(...args)) as ReturnType + } + + onScopeDispose(dispose) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return state! + } + + return wrapped +} + +export default createSharedComposable From 6c549b5097227a16902ffb2b22e478689b172f97 Mon Sep 17 00:00:00 2001 From: uni-kakurenbo Date: Wed, 29 Oct 2025 16:19:13 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20`useEventListener`=20composable?= =?UTF-8?q?=20=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/dom/useEventListener.ts | 94 +++++++++++++++++++++++++ src/lib/basic/array.ts | 5 ++ src/lib/dom/unrefElement.ts | 14 ++++ src/types/utility.d.ts | 2 + 4 files changed, 115 insertions(+) create mode 100644 src/composables/dom/useEventListener.ts create mode 100644 src/lib/dom/unrefElement.ts diff --git a/src/composables/dom/useEventListener.ts b/src/composables/dom/useEventListener.ts new file mode 100644 index 0000000000..3d2d942883 --- /dev/null +++ b/src/composables/dom/useEventListener.ts @@ -0,0 +1,94 @@ +import { + computed, + onScopeDispose, + toValue, + unref, + watch, + type MaybeRef, + type MaybeRefOrGetter +} from 'vue' +import type { Invocable, MaybeArray } from '/@/types/utility' +import { toArray } from '/@/lib/basic/array' +import { unrefElement } from '/@/lib/dom/unrefElement' + +type Targetable = Window | Document | ShadowRoot | HTMLElement | MediaQueryList + +type WindowEventName = keyof WindowEventMap +type DocumentEventName = keyof DocumentEventMap +type ShadowRootEventName = keyof ShadowRootEventMap +type MediaQueryListEventName = keyof MediaQueryListEventMap + +type EventNames = Target extends Window + ? WindowEventName + : Target extends Document + ? DocumentEventName + : Target extends ShadowRoot + ? ShadowRootEventName + : Target extends MediaQueryList + ? MediaQueryListEventName + : string + +interface EventListenerType { + (evt: E): void +} + +const useEventListener = < + Target extends Targetable, + EventType extends Event = Event +>( + _targets: MaybeRefOrGetter | null | undefined>, + _events: MaybeRefOrGetter>>, + _handlers: MaybeRef>>, + options?: MaybeRefOrGetter +) => { + const targets = computed(() => + toArray(toValue(_targets)) + .map(unrefElement) + .filter(e => !!e) + ) + + const events = computed(() => toArray(toValue(_events))) + const handlers = computed(() => toArray(unref(_handlers))) + + const cleanups: Invocable[] = [] + + const cleanup = () => { + cleanups.forEach(cleanup => cleanup()) + cleanups.length = 0 + } + + const stopWatch = watch( + () => + [targets.value, events.value, handlers.value, toValue(options)] as const, + ([targets, events, handlers, options]) => { + cleanup() + + if (!targets || !events || !handlers) return + + const clonedOptions = structuredClone(options) + + cleanups.push( + ...targets.flatMap(target => + events.flatMap(event => + handlers.map((handler: Invocable) => { + target.addEventListener(event, handler, clonedOptions) + + return () => + target.removeEventListener(event, handler, clonedOptions) + }) + ) + ) + ) + }, + { immediate: true, flush: 'post' } + ) + + onScopeDispose(cleanup, true) + + return () => { + stopWatch() + cleanup() + } +} + +export default useEventListener diff --git a/src/lib/basic/array.ts b/src/lib/basic/array.ts index 1f0eae7d31..74cf8d6fb8 100644 --- a/src/lib/basic/array.ts +++ b/src/lib/basic/array.ts @@ -1,3 +1,5 @@ +import type { MaybeArray } from '/@/types/utility' + /** * 一致するキーの一覧を返す * priorityが0のとき完全一致 @@ -59,6 +61,9 @@ export const pickSomeAroundIndex = ( return arr.slice(first, last + 1) } +export const toArray = (array: MaybeArray): T[] => + Array.isArray(array) ? array : [array] + export const isDefined = (o: T): o is Exclude => { return o !== undefined } diff --git a/src/lib/dom/unrefElement.ts b/src/lib/dom/unrefElement.ts new file mode 100644 index 0000000000..be0a28ece9 --- /dev/null +++ b/src/lib/dom/unrefElement.ts @@ -0,0 +1,14 @@ +import type { MaybeRefOrGetter } from 'vue' +import { toValue } from 'vue' +import { isObjectAndHasKey } from '../basic/object' + +export type UnrefElementReturn = T extends { $el: infer E } ? E : T + +export function unrefElement( + element: MaybeRefOrGetter +): UnrefElementReturn + +export function unrefElement(element: MaybeRefOrGetter) { + const plain = toValue(element) + return isObjectAndHasKey(plain, '$el') ? plain.$el : plain +} diff --git a/src/types/utility.d.ts b/src/types/utility.d.ts index ab03513426..b1648b52a8 100644 --- a/src/types/utility.d.ts +++ b/src/types/utility.d.ts @@ -1,2 +1,4 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Invocable = (...args: any[]) => any + +export type MaybeArray = T | T[] From 415cc97d44960c1a2556b5ec821826a4d37d29e7 Mon Sep 17 00:00:00 2001 From: uni-kakurenbo Date: Tue, 28 Oct 2025 14:43:36 +0900 Subject: [PATCH 03/11] =?UTF-8?q?optimize:=20`useInnerWindowSize`=20?= =?UTF-8?q?=E3=82=92=20`createSharedEffectScope`=20=E3=82=92=E7=94=A8?= =?UTF-8?q?=E3=81=84=E3=81=A6=E6=9B=B8=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/composables/dom/useInnerWindowSize.ts | 46 +++++++++++------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/composables/dom/useInnerWindowSize.ts b/src/composables/dom/useInnerWindowSize.ts index 0f3e7740f0..16df3c59b0 100644 --- a/src/composables/dom/useInnerWindowSize.ts +++ b/src/composables/dom/useInnerWindowSize.ts @@ -1,5 +1,7 @@ import { debounce } from 'throttle-debounce' -import { ref, onMounted, onUnmounted, type ComputedRef } from 'vue' +import { ref, type ComputedRef } from 'vue' +import createSharedComposable from '/@/lib/utils/createSharedComposable' +import useEventListener from './useEventListener' type Fallback = { width?: number @@ -24,34 +26,28 @@ type Return = { height: ComputedRef> } -function useInnerWindowSize({ - fallback = {} as F, - delay = 64 -}: Options = {}) { - const width = ref(fallback.width) - const height = ref(fallback.height) +const useInnerWindowSizeImpl = createSharedComposable( + ({ fallback = {} as F, delay = 64 }: Options) => { + const width = ref(fallback.width) + const height = ref(fallback.height) - const updateSize = debounce(delay, () => { - if (typeof window === 'undefined') return - width.value = window.innerWidth - height.value = window.innerHeight - }) + const updateSize = debounce(delay, () => { + if (typeof window === 'undefined') return + width.value = window.innerWidth + height.value = window.innerHeight + }) - onMounted(() => { updateSize() - window.addEventListener('resize', updateSize) - }) + useEventListener(window, 'resize', updateSize) - onUnmounted(() => { - window.removeEventListener('resize', updateSize) - }) + return { + width, + height + } + } +) - return { - width, - height - } as Return -} - -export { useInnerWindowSize } +const useInnerWindowSize = (options: Options = {}) => + useInnerWindowSizeImpl(options) as Return export default useInnerWindowSize From 5385734701d91a49bc1ed45a32576089f97baed3 Mon Sep 17 00:00:00 2001 From: uni-kakurenbo Date: Wed, 29 Oct 2025 23:08:24 +0900 Subject: [PATCH 04/11] =?UTF-8?q?optimize,=20refactor:=20`responsive.ts`?= =?UTF-8?q?=20=E3=82=92=20`createSharedEffectScope`=20=E3=82=92=E7=94=A8?= =?UTF-8?q?=E3=81=84=E3=81=A6=E6=9B=B8=E3=81=8D=E6=8F=9B=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 4 +-- .../Main/CommandPalette/CommandPalette.vue | 4 +-- .../CommandPalette/CommandPaletteInput.vue | 4 +-- .../Main/CommandPalette/SearchSuggestion.vue | 4 +-- .../SearchSuggestionQueryItem.vue | 4 +-- .../ChannelHeaderChannelName.vue | 4 +-- .../ChannelHeader/ChannelHeaderToolsList.vue | 4 +-- .../ChannelHeader/ChannelHeaderToolsMenu.vue | 4 +-- .../MainView/MessageElement/ClipElement.vue | 4 +-- .../MessageElement/MessageElement.vue | 4 +-- .../MainView/MessageElement/MessageTools.vue | 4 +-- .../MainView/MessageElement/StampElement.vue | 4 +-- .../MainView/MessageInput/MessageInput.vue | 4 +-- .../MessageInput/MessageInputLeftControls.vue | 4 +-- .../MessageInput/MessageInputPreview.vue | 4 +-- .../MessageInputRightControls.vue | 4 +-- .../MessageInput/MessageInputTextArea.vue | 4 +-- .../MessageInput/MessageInputTypingUsers.vue | 4 +-- .../PrimaryViewHeader/PrimaryViewHeader.vue | 4 +-- .../PrimaryViewSidebar/PrimaryViewSidebar.vue | 4 +-- .../Main/NavigationBar/NavigationBar.vue | 4 +-- .../Main/StampPicker/StampPicker.vue | 4 +-- .../FeatureContainer/FeatureContainer.vue | 4 +-- .../UserModal/FeatureContainer/LinkButton.vue | 4 +-- src/components/Modal/UserModal/UserModal.vue | 4 +-- src/components/ShareTarget/ShareTarget.vue | 4 +-- .../mainView/useNavigationController.ts | 4 +-- src/composables/mainView/useSidebar.ts | 4 +-- src/composables/useResponsive.ts | 29 ++++++++++++++++++ src/store/ui/responsive.ts | 30 ------------------- src/views/GroupManagerPage.vue | 4 +-- src/views/MainPage.vue | 4 +-- src/views/SettingsPage.vue | 4 +-- src/views/composables/useMainViewLayout.ts | 4 +-- 34 files changed, 93 insertions(+), 94 deletions(-) create mode 100644 src/composables/useResponsive.ts delete mode 100644 src/store/ui/responsive.ts diff --git a/src/App.vue b/src/App.vue index 11241e9bc0..69c473cab9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,11 +16,11 @@ import type { Ref } from 'vue' import { computed, watch, watchEffect } from 'vue' import useHtmlDataset from '/@/composables/document/useHtmlDataset' import { useThemeVariables } from '/@/composables/document/useThemeVariables' -import { useResponsiveStore } from '/@/store/ui/responsive' import { useBrowserSettings } from '/@/store/app/browserSettings' import { useTts } from '/@/store/app/tts' import { useThemeSettings } from '/@/store/app/themeSettings' import useDocumentTitle from '/@/composables/document/useDocumentTitle' +import useResponsive from '/@/composables/useResponsive' const useQallConfirmer = () => { window.addEventListener('beforeunload', event => { @@ -105,7 +105,7 @@ watch( useTts() -const { isMobile } = useResponsiveStore() +const { isMobile } = useResponsive() useDocumentTitle() useQallConfirmer() diff --git a/src/components/Main/CommandPalette/CommandPalette.vue b/src/components/Main/CommandPalette/CommandPalette.vue index 5b68353288..1c981a3b21 100644 --- a/src/components/Main/CommandPalette/CommandPalette.vue +++ b/src/components/Main/CommandPalette/CommandPalette.vue @@ -18,13 +18,13 @@ diff --git a/src/components/Main/MainView/PrimaryViewSidebar/PrimaryViewSidebar.vue b/src/components/Main/MainView/PrimaryViewSidebar/PrimaryViewSidebar.vue index 1f0544c0d4..5f8804621e 100644 --- a/src/components/Main/MainView/PrimaryViewSidebar/PrimaryViewSidebar.vue +++ b/src/components/Main/MainView/PrimaryViewSidebar/PrimaryViewSidebar.vue @@ -15,12 +15,12 @@ diff --git a/src/components/Main/NavigationBar/NavigationBar.vue b/src/components/Main/NavigationBar/NavigationBar.vue index 702a5e24d9..cf19a3891c 100644 --- a/src/components/Main/NavigationBar/NavigationBar.vue +++ b/src/components/Main/NavigationBar/NavigationBar.vue @@ -8,9 +8,9 @@