Skip to content

Commit

Permalink
- prop postIDSelector in favor of using the generic component to re…
Browse files Browse the repository at this point in the history
…strict the type of other props @ `<PostCommonMetadataIconLinks>`

- prop `badgeColor` in favor of adding class in its usages @ `<BadgePostTime>`
@ components/Post/badges

+ param `spid` to replace `pidOrSpid` for its usage in `<PostCommonMetadataIconLinks>` @ `tiebaPostLink()`
- inline exported function `emitEventWithNumberValidator()` to its only usages in `components/widgets/<TimeRange>`
@ shared/index.ts`
@ fe
  • Loading branch information
n0099 committed Mar 12, 2024
1 parent c074c9e commit 68cd3f5
Show file tree
Hide file tree
Showing 8 changed files with 44 additions and 39 deletions.
7 changes: 3 additions & 4 deletions fe/src/components/Post/badges/BadgePostTime.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<template>
<span :data-tippy-content="tippyPrefix + dateTime.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)"
:class="`ms-1 fw-normal badge rounded-pill bg-${badgeColor}`">
class="ms-1 fw-normal badge rounded-pill">
{{ dateTime.toRelative({ round: false }) }}
</span>
</template>

<script setup lang="ts">
import type { BootstrapColor, UnixTimestamp } from '@/shared';
import type { UnixTimestamp } from '@/shared';
import { DateTime } from 'luxon';
const props = withDefaults(defineProps<{
time: UnixTimestamp,
tippyPrefix?: string,
badgeColor: BootstrapColor
tippyPrefix?: string
}>(), { tippyPrefix: '' });
const dateTime = DateTime.fromSeconds(props.time);
Expand Down
27 changes: 13 additions & 14 deletions fe/src/components/Post/badges/PostCommonMetadataIconLinks.vue
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
<template>
<a :href="tiebaPostLink(post.tid, postTypeID === 'tid'
? undefined
: postIDSelector())"
<a :href="tiebaPostLink(props.post.tid,
(props.post as Reply | SubReply).pid,
(props.post as SubReply).spid)"
target="_blank" class="badge bg-light rounded-pill link-dark">
<FontAwesomeIcon icon="link" size="lg" class="align-bottom" />
</a>
<a :data-tippy-content="`<h6>${postTypeID}${postIDSelector()}</h6><hr />
首次收录时间:${formatTime(post.createdAt)}<br />
最后更新时间:${formatTime(post.updatedAt ?? post.createdAt)}<br />
最后发现时间:${formatTime(post.lastSeenAt ?? post.updatedAt ?? post.createdAt)}`"
<a :data-tippy-content="`<h6>${postTypeID}${props.post[props.postTypeID]}</h6><hr />
首次收录时间:${formatTime(props.post.createdAt)}<br />
最后更新时间:${formatTime(props.post.updatedAt ?? props.post.createdAt)}<br />
最后发现时间:${formatTime(props.post.lastSeenAt ?? props.post.updatedAt ?? props.post.createdAt)}`"
class="badge bg-light rounded-pill link-dark">
<FontAwesomeIcon icon="info" size="lg" class="align-bottom" />
</a>
</template>
<script setup lang="ts">
<script setup lang="ts" generic="T extends Reply | SubReply | Thread">
import type { Reply, SubReply, Thread } from '@/api/post';
import type { Pid, PostID, Spid, Tid, UnixTimestamp } from '@/shared';
import type { PostID, UnixTimestamp } from '@/shared';
import { tiebaPostLink } from '@/shared';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { DateTime } from 'luxon';
defineProps<{
post: Reply | SubReply | Thread,
postTypeID: PostID,
postIDSelector: () => Pid | Spid | Tid
// https://github.com/vuejs/language-tools/issues/3267
const props = defineProps<{
post: T,
postTypeID: keyof T & PostID & (T extends Thread ? 'tid' : T extends Reply ? 'pid' : T extends SubReply ? 'spid' : '')
}>();
const formatTime = (time: UnixTimestamp) => {
const dateTime = DateTime.fromSeconds(time);
const relative = dateTime.toRelative({ round: false });
Expand Down
4 changes: 2 additions & 2 deletions fe/src/components/Post/renderers/list/ReplyItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<div class="float-end badge bg-light">
<RouterLink :to="{ name: 'post/pid', params: { pid: reply.pid } }"
class="badge bg-light rounded-pill link-dark">只看此楼</RouterLink>
<PostCommonMetadataIconLinks :post="reply" postTypeID="pid" :postIDSelector="() => reply.pid" />
<BadgePostTime :time="reply.postedAt" badgeColor="primary" />
<PostCommonMetadataIconLinks :post="reply" postTypeID="pid" />
<BadgePostTime :time="reply.postedAt" class="bg-primary" />
</div>
</div>
<div :ref="el => el !== null && replyElements.push(el as HTMLElement)"
Expand Down
5 changes: 2 additions & 3 deletions fe/src/components/Post/renderers/list/SubReplyGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
</RouterLink>
<div class="float-end badge bg-light">
<div class="d-inline" :class="{ invisible: hoveringSubReplyID !== subReply.spid }">
<PostCommonMetadataIconLinks :post="subReply" postTypeID="spid"
:postIDSelector="() => subReply.spid" />
<PostCommonMetadataIconLinks :post="subReply" postTypeID="spid" />
</div>
<BadgePostTime :time="subReply.postedAt" badgeColor="info" />
<BadgePostTime :time="subReply.postedAt" class="bg-info" />
</div>
</template>
<div v-viewer.static class="sub-reply-content" v-html="subReply.content" />

Check warning on line 27 in fe/src/components/Post/renderers/list/SubReplyGroup.vue

View workflow job for this annotation

GitHub Actions / eslint

'v-html' directive can lead to XSS attack
Expand Down
10 changes: 5 additions & 5 deletions fe/src/components/Post/renderers/list/ThreadItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<div class="col-auto badge bg-light">
<RouterLink :to="{ name: 'post/tid', params: { tid: thread.tid } }"
class="badge bg-light rounded-pill link-dark">只看此帖</RouterLink>
<PostCommonMetadataIconLinks :post="thread" postTypeID="tid" :postIDSelector="() => thread.tid" />
<BadgePostTime :time="thread.postedAt" tippyPrefix="发帖时间:" badgeColor="success" />
<PostCommonMetadataIconLinks :post="thread" postTypeID="tid" />
<BadgePostTime :time="thread.postedAt" tippyPrefix="发帖时间:" class="bg-success" />
</div>
</div>
<div class="row justify-content-between mt-2">
Expand Down Expand Up @@ -52,7 +52,7 @@
<span class="fw-bold link-dark">{{ renderUsername(thread.authorUid) }}</span>
</RouterLink>
<BadgeUser v-if="getUser(thread.authorUid).currentForumModerator !== null"
:user="getUser(thread.authorUid)" />
:user="getUser(thread.authorUid)" class="ms-1" />
<template v-if="thread.latestReplierUid === null">
<span class="ms-2 fw-normal link-secondary">最后回复:</span>
<span class="fw-bold link-dark">未知用户</span>
Expand All @@ -63,9 +63,9 @@
<span class="fw-bold link-dark">{{ renderUsername(thread.latestReplierUid) }}</span>
</RouterLink>
<BadgeUser v-if="getUser(thread.latestReplierUid).currentForumModerator !== null"
:user="getUser(thread.latestReplierUid)" />
:user="getUser(thread.latestReplierUid)" class="ms-1" />
</template>
<BadgePostTime :time="thread.latestReplyPostedAt" tippyPrefix="最后回复时间:" badgeColor="secondary" />
<BadgePostTime :time="thread.latestReplyPostedAt" tippyPrefix="最后回复时间:" class="bg-secondary" />
</div>
</div>
</div>
Expand Down
15 changes: 11 additions & 4 deletions fe/src/components/Post/renderers/list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@ const scrollToPostListItem = (el: Element) => {
// that would lead to reflow resulting in the element being pushed down or up out of viewport
/** due to {@link document.scrollingElement.scrollTop()} changed a lot */
const tryScroll = () => {
/** not using a passive callback by IntersectionObserverto to prevent {@link Element.getBoundingClientRect()} caused force reflow */
// due to it will only emit once the configured thresholds are reached
const abortRetries = () => { removeEventListener('scrollend', tryScroll) };
setTimeout(abortRetries, 10000);

/** not using a passive callback by {@link IntersectionObserver} to prevent {@link Element.getBoundingClientRect()} */
// caused force reflow due to it will only emit once the configured thresholds are reached
// thus the top offset might be far from 0 that is top aligned with viewport when the callback is called
// since the element is still near the bottom of viewport at that point of time
// even if the thresholds steps by each percentage like [0.01, 0.02, ..., 1] to let triggers callback more often
// 1% of a very high element is still a big number that may not emit when scrolling ends
// and the element reached the top of viewport
const elTop = el.getBoundingClientRect().top;
const replyTitleTopOffset = getReplyTitleTopOffset();
if (elTop > 0 && Math.abs(elTop) < replyTitleTopOffset + (window.innerHeight * 0.05)) // at most 5dvh tolerance
removeEventListener('scrollend', tryScroll);
if (!el.isConnected // dangling reference to element that already removed from the document
|| window.innerHeight + window.scrollY + (window.innerHeight * 0.01) // at most 1dvh tolerance
>= document.documentElement.scrollHeight // https://stackoverflow.com/questions/3962558/javascript-detect-scroll-end/4638434#comment137130726_4638434
|| (elTop > 0 // element is below the top of viewport
&& Math.abs(elTop) < replyTitleTopOffset + (window.innerHeight * 0.05))) // at most 5dvh tolerance
abortRetries();
else
document.documentElement.scrollBy({ top: elTop - replyTitleTopOffset });
};
Expand Down
6 changes: 3 additions & 3 deletions fe/src/components/widgets/TimeRange.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
<script setup lang="ts">
import type { Dayjs } from 'dayjs';
import type { DurationLike } from 'luxon';
import { emitEventWithNumberValidator } from '@/shared';
import { ref, watchEffect } from 'vue';
import { RangePicker } from 'ant-design-vue';
import dayjs, { unix } from 'dayjs';
import { DateTime } from 'luxon';
import * as _ from 'lodash-es';
defineOptions({ inheritAttrs: true });
const props = withDefaults(defineProps<{
Expand All @@ -33,8 +33,8 @@ const props = withDefaults(defineProps<{
});
// eslint-disable-next-line vue/define-emits-declaration
const emit = defineEmits({
'update:startTime': emitEventWithNumberValidator,
'update:endTime': emitEventWithNumberValidator
'update:startTime': i => _.isNumber(i),
'update:endTime': i => _.isNumber(i)
});
const timeRange = ref<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
Expand Down
9 changes: 5 additions & 4 deletions fe/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ export const notyShow = (type: Noty.Type, text: string) => { new Noty({ timeout:
export const titleTemplate = (title: string) => `${title} - open-tbm @ ${import.meta.env.VITE_INSTANCE_NAME}`;
export const cursorTemplate = (cursor: Cursor) => (cursor === '' ? '起始页' : `页游标 ${cursor}`);

export const tiebaPostLink = (tid: Tid, pidOrSpid?: Pid | Spid) => {
if (pidOrSpid !== undefined)
return `https://tieba.baidu.com/p/${tid}?pid=${pidOrSpid}#${pidOrSpid}`;
export const tiebaPostLink = (tid: Tid, pid?: Pid, spid?: Spid) => {
if (pid !== undefined && spid !== undefined)
return `https://tieba.baidu.com/p/${tid}?pid=${pid}&cid=${spid}#${spid}`;
if (pid !== undefined)
return `https://tieba.baidu.com/p/${tid}?pid=${pid}#${pid}`;

return `https://tieba.baidu.com/p/${tid}`;
};
Expand All @@ -62,7 +64,6 @@ export const boolPropToStr = <T>(object: Record<string, T | boolean>): Record<st
export const boolStrToBool = <T>(s: T | 'false' | 'true'): boolean => s === 'true';
export const boolStrPropToBool = <T>(object: Record<string, T | string>): Record<string, T | boolean | string> =>
_.mapValues(object, i => (_.includes(['true', 'false'], i) ? boolStrToBool(i) : i));
export const emitEventWithNumberValidator = (p: number) => _.isNumber(p);
export const isElementNode = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE;

// https://stackoverflow.com/questions/36532307/rem-px-in-javascript/42769683#42769683
Expand Down

0 comments on commit 68cd3f5

Please sign in to comment.