This commit is contained in:
mattyatea 2024-05-29 01:19:08 +09:00
parent 07b4338eff
commit 653f5cfbc2
13 changed files with 203 additions and 265 deletions

View File

@ -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"`);
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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<typeof meta, typeof paramDef> { // 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;
}

View File

@ -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,

View File

@ -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}`)

View File

@ -43,21 +43,28 @@ export default defineComponent({
setup(props, { slots, expose }) {
const $style = useCssModule(); // 使
const dateTextCache = new Map<string, string>();
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 el = slots.default({
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;
@ -102,6 +109,7 @@ export default defineComponent({
}
}
});
};
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
@ -246,5 +252,8 @@ export default defineComponent({
.date-2-icon {
margin-left: 8px;
}
</style>
.before-leave {
position: absolute !important;
}
</style>

View File

@ -45,7 +45,6 @@ const props = defineProps<{
disableAutoLoad?: boolean;
withCw?: boolean;
}>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
defineExpose({

View File

@ -234,69 +234,43 @@ const reload = (): Promise<void> => {
return init();
};
const fetchMore = async (): Promise<void> => {
async function fetchMore(): Promise<void> {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
try {
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
const response = await misskeyApi<MisskeyEntity[]>(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;
}
...(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(() => {
await nextTick();
if (scrollableElement.value) {
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
scroll(scrollableElement.value, {
top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight),
behavior: 'instant',
});
}
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
items.value = concatMapWithArray(items.value, response);
}
return nextTick();
});
};
if (res.length === 0) {
if (props.pagination.reversed) {
reverseConcat(res).then(() => {
more.value = false;
moreFetching.value = false;
});
} 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;
more.value = response.length > 0;
} catch (error) {
console.error(error);
} finally {
moreFetching.value = false;
}
}
offset.value += res.length;
}, err => {
moreFetching.value = false;
});
};
const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;

View File

@ -1,8 +1,3 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
<MkNotes
@ -55,24 +50,13 @@ const emit = defineEmits<{
provide('inTimeline', true);
provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = {
antennaId?: string,
withRenotes?: boolean,
withReplies?: boolean,
withFiles?: boolean,
visibility?: string,
listId?: string,
channelId?: string,
roleId?: string
}
const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
let tlNotesCount = 0;
function prepend(note) {
if (tlComponent.value == null) return;
if (!tlComponent.value) return;
tlNotesCount++;
@ -96,11 +80,8 @@ let paginationQuery: Paging | null = null;
const stream = useStream();
function connectChannel() {
if (props.src === 'antenna') {
if (props.antenna == null) return;
connection = stream.useChannel('antenna', {
antennaId: props.antenna,
});
if (props.src === 'antenna' && props.antenna) {
connection = stream.useChannel('antenna', { antennaId: props.antenna });
} else if (props.src === 'home') {
connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes,
@ -119,15 +100,9 @@ function connectChannel() {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
});
connection.on('note', prepend);
} else if (props.src === 'social') {
connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
});
} else if (props.src === 'global') {
connection = stream.useChannel('globalTimeline', {
} else if (props.src === 'social' || props.src === 'global') {
const channel = props.src === 'social' ? 'hybridTimeline' : 'globalTimeline';
connection = stream.useChannel(channel, {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
});
@ -136,34 +111,25 @@ function connectChannel() {
connection.on('mention', prepend);
} else if (props.src === 'directs') {
const onNote = note => {
if (note.visibility === 'specified') {
prepend(note);
}
if (note.visibility === 'specified') prepend(note);
};
connection = stream.useChannel('main');
connection.on('mention', onNote);
} else if (props.src === 'list') {
if (props.list == null) return;
} else if (props.src === 'list' && props.list) {
connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
});
} else if (props.src === 'channel') {
if (props.channel == null) return;
connection = stream.useChannel('channel', {
channelId: props.channel,
});
} else if (props.src === 'role') {
if (props.role == null) return;
connection = stream.useChannel('roleTimeline', {
roleId: props.role,
});
} else if (props.src === 'channel' && props.channel) {
connection = stream.useChannel('channel', { channelId: props.channel });
} else if (props.src === 'role' && props.role) {
connection = stream.useChannel('roleTimeline', { roleId: props.role });
}
if (props.src.startsWith('custom-timeline')) {
return;
if (props.src !== 'directs' && props.src !== 'mentions') {
connection?.on('note', prepend);
}
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
}
function disconnectChannel() {
@ -172,91 +138,47 @@ function disconnectChannel() {
}
function updatePaginationQuery() {
let endpoint: keyof Misskey.Endpoints | null;
let query: TimelineQueryType | null;
const endpoints = {
antenna: 'antennas/notes',
home: 'notes/timeline',
local: 'notes/local-timeline',
social: 'notes/hybrid-timeline',
global: 'notes/global-timeline',
media: 'notes/hybrid-timeline',
mentions: 'notes/mentions',
directs: 'notes/mentions',
list: 'notes/user-list-timeline',
channel: 'channels/timeline',
role: 'roles/notes',
};
if (props.src === 'antenna') {
endpoint = 'antennas/notes';
query = {
antennaId: props.antenna,
const queries = {
antenna: { antennaId: props.antenna },
home: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined },
local: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined },
social: { withRenotes: props.withRenotes, withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined },
global: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined },
media: { withFiles: true, withRenotes: props.withRenotes, withReplies: false },
mentions: null,
directs: { visibility: 'specified' },
list: { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, listId: props.list },
channel: { channelId: props.channel },
role: { roleId: props.role },
};
} else if (props.src === 'home') {
endpoint = 'notes/timeline';
query = {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
};
} else if (props.src === 'local') {
endpoint = 'notes/local-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
} else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
query = {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
};
} else if (props.src === 'media') {
endpoint = 'notes/hybrid-timeline';
query = {
withFiles: true,
withRenotes: props.withRenotes,
withReplies: false,
};
} else if (props.src === 'mentions') {
endpoint = 'notes/mentions';
query = null;
} else if (props.src === 'directs') {
endpoint = 'notes/mentions';
query = {
visibility: 'specified',
};
} else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline';
query = {
withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
};
} else if (props.src === 'channel') {
endpoint = 'channels/timeline';
query = {
channelId: props.channel,
};
} else if (props.src === 'role') {
endpoint = 'roles/notes';
query = {
roleId: props.role,
};
} else if (props.src.startsWith('custom-timeline')) {
endpoint = 'notes/any-local-timeline';
query = {
if (props.src.startsWith('custom-timeline')) {
paginationQuery = {
endpoint: 'notes/any-local-timeline',
limit: 10,
params: {
host: defaultStore.state[`remoteLocalTimelineDomain${props.src.split('-')[2]}`],
remoteToken: defaultStore.state[`remoteLocalTimelineToken${props.src.split('-')[2]}`],
},
};
} else {
endpoint = null;
query = null;
}
if (endpoint && query) {
paginationQuery = {
endpoint: endpoint,
limit: 10,
params: query,
};
} else {
paginationQuery = null;
const endpoint = endpoints[props.src];
const query = queries[props.src];
paginationQuery = endpoint && query ? { endpoint, limit: 10, params: query } : null;
}
}
@ -265,12 +187,9 @@ function refreshEndpointAndChannel() {
disconnectChannel();
connectChannel();
}
updatePaginationQuery();
}
// withRenotes
// IDTL
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
//
@ -282,13 +201,11 @@ onUnmounted(() => {
function reloadTimeline() {
return new Promise<void>((res) => {
if (tlComponent.value == null) return;
if (!tlComponent.value) return;
tlNotesCount = 0;
tlComponent.value.pagingComponent?.reload().then(() => {
res();
});
tlComponent.value.pagingComponent?.reload().then(() => res());
});
}

View File

@ -32,6 +32,8 @@ export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {}
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
export const googleAnalyticsId = computed(() => instance.googleAnalyticsId ?? null);
export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);

View File

@ -77,6 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.somethingHappened }}</template>
</MkInput>
<MkInput v-model="googleAnalyticsId" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>googleAnal </template>
</MkInput>
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
@ -144,6 +148,8 @@ const themeColor = ref<string | null>(null);
const defaultLightTheme = ref<string | null>(null);
const defaultDarkTheme = ref<string | null>(null);
const serverErrorImageUrl = ref<string | null>(null);
const googleAnalyticsId = ref<string | null>(null);
const infoImageUrl = ref<string | null>(null);
const notFoundImageUrl = ref<string | null>(null);
const repositoryUrl = ref<string | null>(null);
@ -189,6 +195,7 @@ function save() {
infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
googleAnalyticsId: googleAnalyticsId.value === '' ? null : googleAnalyticsId.value,
repositoryUrl: repositoryUrl.value === '' ? null : repositoryUrl.value,
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),