feat: note time travel
Some checks failed
Lint / lint (frontend) (push) Blocked by required conditions
Lint / lint (frontend-embed) (push) Blocked by required conditions
Lint / lint (frontend-shared) (push) Blocked by required conditions
Lint / lint (misskey-bubble-game) (push) Blocked by required conditions
Lint / lint (misskey-js) (push) Blocked by required conditions
Lint / lint (misskey-reversi) (push) Blocked by required conditions
Lint / lint (sw) (push) Blocked by required conditions
Lint / typecheck (backend) (push) Blocked by required conditions
Lint / typecheck (misskey-js) (push) Blocked by required conditions
Lint / typecheck (sw) (push) Blocked by required conditions
Check SPDX-License-Identifier / check-spdx-license-id (push) Failing after 46s
Check copyright year / check_copyright_year (push) Successful in 46s
Dockle / dockle (push) Successful in 1m26s
Lint / pnpm_install (push) Successful in 2m45s
Publish Docker image (develop) / Build (linux/amd64) (push) Waiting to run
Publish Docker image (develop) / merge (push) Blocked by required conditions
Lint / locale_verify (push) Successful in 2m45s
Storybook / build (push) Has been skipped
Test (frontend) / vitest (20.16.0) (push) Failing after 3m17s
Test (frontend) / e2e (chrome, 20.16.0) (push) Failing after 3m47s
Test (production install and build) / production (20.16.0) (push) Failing after 1m44s
Lint / lint (backend) (push) Has been cancelled

This commit is contained in:
Lhc_fl 2024-10-06 20:48:49 +08:00 committed by laoXong
parent b6dff11a52
commit 7686cd3eac
6 changed files with 127 additions and 28 deletions

View File

@ -1294,6 +1294,8 @@ _abuseUserReport:
accept: "Accept"
reject: "Reject"
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
timeTravel: Time Travel
timeTravelDescription: Show posts before this date
_delivery:
status: "Delivery status"
stop: "Suspended"

View File

@ -1300,6 +1300,8 @@ _abuseUserReport:
accept: "确认"
reject: "拒绝"
resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚选择「拒绝」将案件以否定的态度标记为已解决。"
timeTravel: 时光机
timeTravelDescription: 显示该日期以前的帖子
_delivery:
status: "投递状态"
stop: "停止投递"

View File

@ -61,7 +61,8 @@ type TimelineQueryType = {
visibility?: string,
listId?: string,
channelId?: string,
roleId?: string
roleId?: string,
untilDate?: number,
}
const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
@ -89,7 +90,7 @@ function prepend(note) {
let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null;
const paginationQuery = ref<Paging | null>(null);
const stream = useStream();
@ -124,7 +125,7 @@ function connectChannel() {
});
} else if (props.src === 'mentions') {
connection = stream.useChannel('main');
connection.on('mention', prepend);
connection?.on('mention', prepend);
} else if (props.src === 'directs') {
const onNote = note => {
if (note.visibility === 'specified') {
@ -132,7 +133,7 @@ function connectChannel() {
}
};
connection = stream.useChannel('main');
connection.on('mention', onNote);
connection?.on('mention', onNote);
} else if (props.src === 'list') {
if (props.list == null) return;
connection = stream.useChannel('userList', {
@ -159,7 +160,7 @@ function disconnectChannel() {
if (connection2) connection2.dispose();
}
function updatePaginationQuery() {
function updatePaginationQuery(untilDate?: Date) {
let endpoint: keyof Misskey.Endpoints | null;
let query: TimelineQueryType | null;
@ -224,14 +225,22 @@ function updatePaginationQuery() {
query = null;
}
if (untilDate) {
query = query ?? {};
query.untilDate = Number(untilDate);
} else {
query = query ?? {};
query.untilDate = undefined;
}
if (endpoint && query) {
paginationQuery = {
paginationQuery.value = {
endpoint: endpoint,
limit: 10,
params: query,
};
} else {
paginationQuery = null;
paginationQuery.value = null;
}
}
@ -267,7 +276,12 @@ function reloadTimeline() {
});
}
function timetravel(date: Date) {
updatePaginationQuery(date);
}
defineExpose({
reloadTimeline,
timetravel,
});
</script>

View File

@ -56,6 +56,8 @@ const props = withDefaults(defineProps<{
actions?: PageHeaderItem[] | null;
thin?: boolean;
displayMyAvatar?: boolean;
displayBackButton?: boolean;
hideTitle?: boolean;
}>(), {
tabs: () => ([] as Tab[]),
});
@ -66,7 +68,7 @@ const emit = defineEmits<{
const pageMetadata = injectReactiveMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const hideTitle = props.hideTitle || inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = shallowRef<HTMLElement | undefined>(undefined);

View File

@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import type { BasicTimelineType } from '@/timelines.js';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
@ -53,7 +54,6 @@ import { deepMerge } from '@/scripts/merge.js';
import type { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
import type { BasicTimelineType } from '@/timelines.js';
provide('shouldOmitHeaderTitle', true);
@ -221,11 +221,12 @@ function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue:
async function timetravel(): Promise<void> {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.date,
title: i18n.ts.timeTravel as string,
text: i18n.ts.timeTravelDescription as string,
});
if (canceled) return;
tlComponent.value.timetravel(date);
tlComponent.value?.timetravel(date);
}
function focus(): void {
@ -284,9 +285,16 @@ const headerActions = computed(() => {
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
});
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
}, {
type: 'divider',
}, {
type: 'button',
text: i18n.ts.timeTravel,
icon: 'ti ti-calendar-time',
action: () => {
timetravel();
},
}], ev.currentTarget ?? ev.target);
},
},
];

View File

@ -6,23 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header>
<MkTab v-model="tab" :class="$style.tab">
<option value="featured">{{ i18n.ts.featured }}</option>
<option :value="null">{{ i18n.ts.notes }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="files">{{ i18n.ts.withFiles }}</option>
</MkTab>
<MkPageHeader v-model:tab="tab" :class="$style.tab" :actions="headerActions" :tabs="headerTabs" hideTitle/>
</template>
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl"/>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import MkTab from '@/components/MkTab.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
@ -30,6 +25,12 @@ const props = defineProps<{
const tab = ref<string | null>('all');
const timetraveled = ref<Date>();
const withRenotes = ref(true);
const onlyFiles = ref(false);
const withReplies = ref(true);
const withChannelNotes = ref(true);
const pagination = computed(() => tab.value === 'featured' ? {
endpoint: 'users/featured-notes' as const,
limit: 10,
@ -41,18 +42,88 @@ const pagination = computed(() => tab.value === 'featured' ? {
limit: 10,
params: {
userId: props.user.id,
withRenotes: tab.value === 'all',
withReplies: tab.value === 'all',
withChannelNotes: tab.value === 'all',
withFiles: tab.value === 'files',
withRenotes: withRenotes.value,
withReplies: withReplies.value,
withChannelNotes: withChannelNotes.value,
withFiles: onlyFiles.value,
untilDate: timetraveled.value ? Number(timetraveled.value) : undefined,
},
});
async function timetravel(): Promise<void> {
const { canceled, result: date } = await os.inputDate({
title: i18n.ts.timeTravel as string,
text: i18n.ts.timeTravelDescription as string,
});
if (canceled) return;
timetraveled.value = date;
}
watch(withReplies, (nv) => {
if (nv && onlyFiles.value) {
onlyFiles.value = false;
}
});
watch(onlyFiles, (nv) => {
if (withReplies.value && nv) {
withReplies.value = false;
}
});
const headerActions = computed(() => [
{
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([
{
type: 'switch',
text: i18n.ts.withReplies,
ref: withReplies,
}, {
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, {
type: 'switch',
text: i18n.ts.channel,
ref: withChannelNotes,
}, {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
}, {
type: 'divider',
}, {
type: 'button',
text: i18n.ts.timeTravel,
icon: 'ti ti-calendar-time',
action: () => {
timetravel();
},
}], ev.currentTarget ?? ev.target);
},
},
]);
const headerTabs = computed(() => [
{
key: 'featured',
title: i18n.ts.featured,
icon: 'ph-lightbulb ph-bold ph-lg',
},
{
key: 'all',
title: i18n.ts.notes,
icon: 'ph-pencil-simple ph-bold ph-lg',
},
]);
</script>
<style lang="scss" module>
.tab {
padding: calc(var(--MI-margin) / 2) 0;
background: var(--MI_THEME-bg);
// padding: calc(var(--margin) / 2) 0;
// background: var(--bg);
}
.tl {