feat: note time travel
This commit is contained in:
parent
b6dff11a52
commit
c76b17e4c0
@ -1294,6 +1294,13 @@ _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."
|
||||
stpvAntennaPatch: |
|
||||
Sharkey Stelpolva Edition adds additional keyword syntax to the antenna settings.
|
||||
That means, the special keyword `domain:example.com` will match all posts from `example.com`. This keyword can also be used in OR syntax.
|
||||
Specifically, you must use `domain:here` to match posts in this instance.
|
||||
stpvPgroongaSearchInstru: Use the `A OR B` syntax to search for notes containing either A or B. `A B` (with a space in between) means posts containing both keywords A and B.
|
||||
timeTravel: Time Travel
|
||||
timeTravelDescription: Show posts before this date
|
||||
_delivery:
|
||||
status: "Delivery status"
|
||||
stop: "Suspended"
|
||||
|
@ -1300,6 +1300,13 @@ _abuseUserReport:
|
||||
accept: "确认"
|
||||
reject: "拒绝"
|
||||
resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚,选择「拒绝」将案件以否定的态度标记为已解决。"
|
||||
stpvAntennaPatch: |
|
||||
Sharkey Stelpolva Edition 为天线的设置添加了额外的关键词语法。
|
||||
具体而言, `domain:example.com` 这个特殊关键词会匹配所有来自 `example.com` 的帖子。这个关键词同样能使用在 OR 语法中。
|
||||
特别地,你必须用 `domain:here` 来匹配本实例的帖子。
|
||||
stpvPgroongaSearchInstru: 可以使用 `A OR B` 的词法来搜索包含 A 或者 B 的帖子。`A B`(中间有空格)代表同时包含关键词 A 和 B 的帖子。
|
||||
timeTravel: 时光机
|
||||
timeTravelDescription: 显示该日期以前的帖子
|
||||
_delivery:
|
||||
status: "投递状态"
|
||||
stop: "停止投递"
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
},
|
||||
}); os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user