diff --git a/packages/backend/migration/1716911535226-gapikey.js b/packages/backend/migration/1716911535226-gapikey.js new file mode 100644 index 0000000000..5ec4594aeb --- /dev/null +++ b/packages/backend/migration/1716911535226-gapikey.js @@ -0,0 +1,11 @@ +export class Gapikey1716911535226 { + name = 'Gapikey1716911535226' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "googleAnalyticsId" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "googleAnalyticsId"`); + } +} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 50669e1a3c..363f8f0d2d 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -73,6 +73,7 @@ export class MetaEntityService { bannerLight: instance.bannerLight, iconDark: instance.iconDark, iconLight: instance.iconLight, + googleAnalyticsId: instance.googleAnalyticsId, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableMcaptcha: instance.enableMcaptcha, @@ -88,6 +89,7 @@ export class MetaEntityService { bannerUrl: instance.bannerUrl, infoImageUrl: instance.infoImageUrl, serverErrorImageUrl: instance.serverErrorImageUrl, + googleAnalyticsId: instance.googleAnalyticsId, notFoundImageUrl: instance.notFoundImageUrl, iconUrl: instance.iconUrl, backgroundImageUrl: instance.backgroundImageUrl, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 1b3992abe7..1205553436 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -139,6 +139,11 @@ export class MiMeta { nullable: true, }) public serverErrorImageUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public googleAnalyticsId: string | null; @Column('varchar', { length: 1024, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index dad4ac7287..0849a71ae8 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -50,6 +50,7 @@ export const paramDef = { mascotImageUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true }, serverErrorImageUrl: { type: 'string', nullable: true }, + googleAnalyticsId: { type: 'string', nullable: true }, infoImageUrl: { type: 'string', nullable: true }, notFoundImageUrl: { type: 'string', nullable: true }, iconUrl: { type: 'string', nullable: true }, @@ -257,7 +258,9 @@ export default class extends Endpoint { // eslint- if (ps.serverErrorImageUrl !== undefined) { set.serverErrorImageUrl = ps.serverErrorImageUrl; } - + if (ps.googleAnalyticsId !== undefined) { + set.googleAnalyticsId = ps.googleAnalyticsId; + } if (ps.enableProxyCheckio !== undefined) { set.enableProxyCheckio = ps.enableProxyCheckio; } diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 22709dbca7..e7117b9438 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -191,6 +191,7 @@ export class ClientServerService { appleTouchIcon: meta.app512IconUrl, themeColor: meta.themeColor, serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg', + googleAnalyticsId: meta.googleAnalyticsId ?? null, infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', instanceUrl: this.config.url, diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index ec1325e4e9..7e519b37ed 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -35,6 +35,14 @@ html link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) + if googleAnalyticsId + script(async src='https://www.googletagmanager.com/gtag/js?id='+ googleAnalyticsId) + script. + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '#{googleAnalyticsId}'); + //- https://github.com/misskey-dev/misskey/issues/9842 link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v3.3.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 5cb19f388a..4118a6f81c 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -96,7 +96,7 @@ export async function mainBoot() { }).render(); } } - } + } } catch (error) { // console.error(error); console.error('Failed to initialise the seasonal screen effect canvas context:', error); diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 85e131cf9b..3e17368a58 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -43,65 +43,73 @@ export default defineComponent({ setup(props, { slots, expose }) { const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫 + const dateTextCache = new Map(); + function getDateText(time: string) { + if (dateTextCache.has(time)) { + return dateTextCache.get(time)!; + } const date = new Date(time).getDate(); const month = new Date(time).getMonth() + 1; - return i18n.tsx.monthAndDay({ + const text = i18n.tsx.monthAndDay({ month: month.toString(), day: date.toString(), }); + dateTextCache.set(time, text); + return text; } if (props.items.length === 0) return; - const renderChildrenImpl = () => props.items.map((item, i) => { - if (!slots || !slots.default) return; + const renderChildrenImpl = () => { + const slotContent = slots.default ? slots.default : () => []; + return props.items.map((item, i) => { + const el = slotContent({ + item: item, + })[0]; + if (el.key == null && item.id) el.key = item.id; - const el = slots.default({ - item: item, - })[0]; - if (el.key == null && item.id) el.key = item.id; - - if ( - i !== props.items.length - 1 && - new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() - ) { - const separator = h('div', { - class: $style['separator'], - key: item.id + ':separator', - }, h('p', { - class: $style['date'], - }, [ - h('span', { - class: $style['date-1'], + if ( + i !== props.items.length - 1 && + new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() + ) { + const separator = h('div', { + class: $style['separator'], + key: item.id + ':separator', + }, h('p', { + class: $style['date'], }, [ - h('i', { - class: `ti ti-chevron-up ${$style['date-1-icon']}`, - }), - getDateText(item.createdAt), - ]), - h('span', { - class: $style['date-2'], - }, [ - getDateText(props.items[i + 1].createdAt), - h('i', { - class: `ti ti-chevron-down ${$style['date-2-icon']}`, - }), - ]), - ])); + h('span', { + class: $style['date-1'], + }, [ + h('i', { + class: `ti ti-chevron-up ${$style['date-1-icon']}`, + }), + getDateText(item.createdAt), + ]), + h('span', { + class: $style['date-2'], + }, [ + getDateText(props.items[i + 1].createdAt), + h('i', { + class: `ti ti-chevron-down ${$style['date-2-icon']}`, + }), + ]), + ])); - return [el, separator]; - } else { - if (props.ad && item._shouldInsertAd_) { - return [h(MkAd, { - key: item.id + ':ad', - prefer: ['horizontal', 'horizontal-big'], - }), el]; + return [el, separator]; } else { - return el; + if (props.ad && item._shouldInsertAd_) { + return [h(MkAd, { + key: item.id + ':ad', + prefer: ['horizontal', 'horizontal-big'], + }), el]; + } else { + return el; + } } - } - }); + }); + }; const renderChildren = () => { const children = renderChildrenImpl(); @@ -120,14 +128,12 @@ export default defineComponent({ function onBeforeLeave(element: Element) { const el = element as HTMLElement; - el.style.top = `${el.offsetTop}px`; - el.style.left = `${el.offsetLeft}px`; + el.classList.add('before-leave'); } function onLeaveCancelled(element: Element) { const el = element as HTMLElement; - el.style.top = ''; - el.style.left = ''; + el.classList.remove('before-leave'); } // eslint-disable-next-line vue/no-setup-props-destructure @@ -157,21 +163,21 @@ export default defineComponent({ container-type: inline-size; &:global { - > .list-move { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } + > .list-move { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - &.deny-move-transition > .list-move { - transition: none !important; - } + &.deny-move-transition > .list-move { + transition: none !important; + } - > .list-enter-active { - transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); - } + > .list-enter-active { + transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1); + } - > *:empty { - display: none; - } + > *:empty { + display: none; + } } &:not(.date-separated-list-nogap) > *:not(:last-child) { @@ -194,20 +200,20 @@ export default defineComponent({ .direction-up { &:global { - > .list-enter-from, - > .list-leave-to { - opacity: 0; - transform: translateY(64px); - } + > .list-enter-from, + > .list-leave-to { + opacity: 0; + transform: translateY(64px); + } } } .direction-down { &:global { - > .list-enter-from, - > .list-leave-to { - opacity: 0; - transform: translateY(-64px); - } + > .list-enter-from, + > .list-leave-to { + opacity: 0; + transform: translateY(-64px); + } } } @@ -246,5 +252,8 @@ export default defineComponent({ .date-2-icon { margin-left: 8px; } - +.before-leave { + position: absolute !important; +} + diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index ef5ad766b1..11c99a2cd9 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :ad="true" :class="$style.notes" > - + @@ -45,7 +45,6 @@ const props = defineProps<{ disableAutoLoad?: boolean; withCw?: boolean; }>(); - const pagingComponent = shallowRef>(); defineExpose({ diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 19e22f78d0..fc1ec0989a 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -234,69 +234,43 @@ const reload = (): Promise => { return init(); }; -const fetchMore = async (): Promise => { +async function fetchMore(): Promise { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; + moreFetching.value = true; - const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; - await misskeyApi(props.pagination.endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT, - ...(props.pagination.offsetMode ? { - offset: offset.value, - } : { - untilId: Array.from(items.value.keys()).at(-1), - }), - }).then(res => { - for (let i = 0; i < res.length; i++) { - const item = res[i]; - if (i === 10) item._shouldInsertAd_ = true; - } + try { + const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + const response = await misskeyApi(props.pagination.endpoint, { + ...params, + limit: SECOND_FETCH_LIMIT, + ...(props.pagination.offsetMode ? { offset: offset.value } : { untilId: Array.from(items.value.keys()).pop() }), + }); - const reverseConcat = _res => { - const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); - const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; + const isReversed = props.pagination.reversed; + if (isReversed) { + const oldHeight = scrollableElement.value?.scrollHeight || 0; + const oldScroll = scrollableElement.value?.scrollTop || 0; - items.value = concatMapWithArray(items.value, _res); + items.value = concatMapWithArray(items.value, response); - return nextTick(() => { - if (scrollableElement.value) { - scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); - } else { - window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); - } - - return nextTick(); - }); - }; - - if (res.length === 0) { - if (props.pagination.reversed) { - reverseConcat(res).then(() => { - more.value = false; - moreFetching.value = false; + await nextTick(); + if (scrollableElement.value) { + scroll(scrollableElement.value, { + top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), + behavior: 'instant', }); - } else { - items.value = concatMapWithArray(items.value, res); - more.value = false; - moreFetching.value = false; } } else { - if (props.pagination.reversed) { - reverseConcat(res).then(() => { - more.value = true; - moreFetching.value = false; - }); - } else { - items.value = concatMapWithArray(items.value, res); - more.value = true; - moreFetching.value = false; - } + items.value = concatMapWithArray(items.value, response); } - offset.value += res.length; - }, err => { + + more.value = response.length > 0; + } catch (error) { + console.error(error); + } finally { moreFetching.value = false; - }); -}; + } +} const fetchMoreAhead = async (): Promise => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 3a65406b1e..4ba1ced710 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -1,8 +1,3 @@ - -