Skip to content

Commit

Permalink
* replace props accepting UnixTimestamp with TPost and `TPostTime…
Browse files Browse the repository at this point in the history
…Key` to get timestamp within the component instead of its usages

* now will only show one of the timestamps that are relative to the previous, next or parent post before the default one that is relative to now
@ `badges/<BadgePostTime>`

+ `getSiblingPostedAt()` to share similar values for props `(previous|next)TimeOverride` of `<BadgePostTime>`
* replace prop `replyAuthoruid` with `reply` for `<BadgePostTime>.parentPost`
@ `<SubReplyGroup>`

* replace prop `threadAuthoruid` with `thread` for `<BadgePostTime>.parentPost` @ `<ReplyItem>` & `<SubReplyGroup>`
@ renderers/list
@ components/Post

+ bundled icons for `<BadgePostTime>` @ fontAwesome.ts

+ `undefinedOr()`
+ exported `postType(Text)` and its types
- unused exported `postTypeToID`
@ index.ts
@ shared
@ fe
  • Loading branch information
n0099 committed Mar 14, 2024
1 parent 9b32477 commit 79c9887
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 64 deletions.
91 changes: 64 additions & 27 deletions fe/src/components/Post/badges/BadgePostTime.vue
Original file line number Diff line number Diff line change
@@ -1,47 +1,84 @@
<template>
<DefineTemplate v-slot="{ base, time, tippyPrefix }">
<DefineTemplate v-slot="{ $slots, base, relativeTo }">
<span :data-tippy-content="`
${base === undefined ? '' : `${base.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}<br>`}
${tippyPrefix}<br>
${time.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}`"
本${postType}${timestampType}:<br>
${currentDateTime.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}<br>
${base === undefined || relativeTo === undefined
? ''
: `${relativeTo}:<br>${base.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}`}`"
class="ms-1 fw-normal badge rounded-pill" v-bind="$attrs">
{{ time.toRelative({
base,
round: false
}) }}
<component :is="$slots.default" />
{{ currentDateTime.toRelative({ base, round: false }) }}
</span>
</DefineTemplate>
<ReuseTemplate v-if="previousPostDateTime !== undefined"
:time="currentPostDateTime" :base="previousPostDateTime" :tippyPrefix="`相对于上一帖${timestampType}`" />
<ReuseTemplate :time="currentPostDateTime" :tippyPrefix="`${timestampType}:`" />
<ReuseTemplate v-if="nextPostDateTime !== undefined"
:time="nextPostDateTime" :base="currentPostDateTime" :tippyPrefix="`相对于下一帖${timestampType}`" />
<ReuseTemplate v-if="previousTime !== undefined && previousTime < currentTime && previousDateTime !== undefined"
:base="previousDateTime" :relativeTo="`相对于下一${postType}${timestampType}`">
<FontAwesomeIcon icon="chevron-down" class="align-bottom" />
</ReuseTemplate>
<ReuseTemplate v-else-if="nextTime !== undefined && nextTime < currentTime && nextDateTime !== undefined"
:base="nextDateTime" :relativeTo="`相对于上一${postType}${timestampType}`">
<FontAwesomeIcon icon="chevron-up" class="align-bottom" />
</ReuseTemplate>
<ReuseTemplate v-else-if="parentTime !== undefined && parentTime !== currentTime"
:base="parentDateTime"
:relativeTo="`相对于所属${postTypeText[postTypeText.indexOf(props.postType) - 1]}${timestampType}`">
<FontAwesomeIcon icon="angles-up" class="align-bottom" />
</ReuseTemplate>
<ReuseTemplate />
</template>

<script setup lang="ts">
import type { UnixTimestamp } from '@/shared';
<script setup lang="ts" generic="
TPost extends Reply | SubReply | Thread,
TParentPost extends TPost extends SubReply ? Reply
: TPost extends Reply ? Thread
: TPost extends Thread ? never : unknown,
TPostTimeKey extends keyof TPost
& keyof TParentPost
& ('postedAt' | (TPost extends Thread ? 'latestReplyPostedAt' : never)),
TPostTimeValue extends TPost['postedAt'] & (TPost extends Thread ? TPost['latestReplyPostedAt'] : unknown)">
import type { Reply, SubReply, Thread } from '@/api/post';
import { postTypeText, undefinedOr } from '@/shared';
import { computed } from 'vue';
import { createReusableTemplate } from '@vueuse/core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { DateTime } from 'luxon';
defineOptions({ inheritAttrs: false });
const props = defineProps<{
timestampType: string,
previousPostTime?: UnixTimestamp,
currentPostTime: UnixTimestamp,
nextPostTime?: UnixTimestamp
previousTimeOverride?: TPostTimeValue,
previousPost?: TPost,
nextTimeOverride?: TPostTimeValue,
nextPost?: TPost,
parentPost?: TParentPost,
currentPost: TPost,
postTimeKey: TPostTimeKey,
timestampType: 'latestReplyPostedAt' extends TPostTimeKey ? '最后回复时间'
: 'postedAt' extends TPostTimeKey ? '发帖时间' : never,
postType: TPost extends Thread ? '主题帖'
: TPost extends Reply ? '回复贴'
: TPost extends SubReply ? '楼中楼' : never
}>();
const [DefineTemplate, ReuseTemplate] = createReusableTemplate<{
base?: DateTime<true>,
time: DateTime<true>,
tippyPrefix: string
relativeTo?: string
}>();
const previousPostDateTime = computed(() =>
(props.previousPostTime === undefined ? undefined : DateTime.fromSeconds(props.previousPostTime)));
const currentPostDateTime = computed(() => DateTime.fromSeconds(props.currentPostTime));
const nextPostDateTime = computed(() =>
(props.nextPostTime === undefined ? undefined : DateTime.fromSeconds(props.nextPostTime)));
const previousTime = computed(() =>
props.previousTimeOverride ?? (props.previousPost?.[props.postTimeKey] as TPostTimeValue | undefined));
const nextTime = computed(() =>
props.nextTimeOverride ?? (props.nextPost?.[props.postTimeKey] as TPostTimeValue | undefined));
const parentTime = computed(() =>
(props.parentPost?.[props.postTimeKey] as TPostTimeValue | undefined));
const currentTime = computed(() =>
(props.currentPost[props.postTimeKey] as TPostTimeValue));
const previousDateTime = computed(() =>
undefinedOr(previousTime.value, i => DateTime.fromSeconds(i)));
const nextDateTime = computed(() =>
undefinedOr(nextTime.value, i => DateTime.fromSeconds(i)));
const parentDateTime = computed(() =>
undefinedOr(parentTime.value, i => DateTime.fromSeconds(i)));
const currentDateTime = computed(() => DateTime.fromSeconds(currentTime.value));
</script>

<style scoped>
Expand Down
23 changes: 10 additions & 13 deletions fe/src/components/Post/renderers/list/ReplyItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
<RouterLink :to="{ name: 'post/pid', params: { pid: reply.pid } }"
class="badge bg-light rounded-pill link-dark">只看此楼</RouterLink>
<PostCommonMetadataIconLinks :post="reply" postTypeID="pid" />
<BadgePostTime :previousPostTime="previousReply?.postedAt"
:currentPostTime="reply.postedAt"
:nextPostTime="nextReply?.postedAt"
timestampType="发帖时间" class="bg-primary" />
<BadgePostTime postType="回复贴" :parentPost="thread"
:previousPost="previousReply" :currentPost="reply" :nextPost="nextReply"
postTimeKey="postedAt" timestampType="发帖时间" class="bg-primary" />
</div>
</div>
<div :ref="el => el !== null && replyElements.push(el as HTMLElement)"
Expand All @@ -33,15 +32,14 @@
<p class="my-0">{{ author.name }}</p>
<p v-if="author.displayName !== null && author.name !== null">{{ author.displayName }}</p>
</RouterLink>
<BadgeUser :user="getUser(reply.authorUid)" :threadAuthorUid="threadAuthorUid" />
<BadgeUser :user="getUser(reply.authorUid)" :threadAuthorUid="thread.authorUid" />
</div>
<div class="col me-2 px-1 border-start overflow-auto">
<div v-viewer.static class="reply-content p-2" v-html="reply.content" />
<template v-if="reply.subReplies.length > 0">
<SubReplyGroup v-for="(subReplyGroup, index) in reply.subReplies" :key="index"
:previousSubReplyGroup="reply.subReplies[index - 1]" :subReplyGroup="subReplyGroup"
:nextSubReplyGroup="reply.subReplies[index + 1]"
:threadAuthorUid="threadAuthorUid" :replyAuthorUid="reply.authorUid" />
:nextSubReplyGroup="reply.subReplies[index + 1]" :thread="thread" :reply="reply" />
</template>
</div>
</div>
Expand All @@ -55,20 +53,19 @@ import SubReplyGroup from './SubReplyGroup.vue';
import BadgePostTime from '@/components/Post/badges/BadgePostTime.vue';
import BadgeUser from '@/components/Post/badges/BadgeUser.vue';
import PostCommonMetadataIconLinks from '@/components/Post/badges/PostCommonMetadataIconLinks.vue';
import type { BaiduUserID } from '@/api/user';
import { toUserPortraitImageUrl, toUserRoute } from '@/shared';
import { useElementRefsStore } from '@/stores/elementRefs';
import '@/styles/bootstrapCallout.css';
import { inject, nextTick, onMounted, ref } from 'vue';
import { RouterLink } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
type Reply = ThreadWithGroupedSubReplies['replies'][number];
type ReplyWithGroupedSubReplies = ThreadWithGroupedSubReplies['replies'][number];
defineProps<{
previousReply?: Reply,
reply: Reply,
nextReply?: Reply,
threadAuthorUid: BaiduUserID
thread: ThreadWithGroupedSubReplies,
previousReply?: ReplyWithGroupedSubReplies,
reply: ReplyWithGroupedSubReplies,
nextReply?: ReplyWithGroupedSubReplies
}>();
const elementRefsStore = useElementRefsStore();
Expand Down
28 changes: 16 additions & 12 deletions fe/src/components/Post/renderers/list/SubReplyGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@
{{ renderUsername(subReply.authorUid) }}
</span>
<BadgeUser :user="getUser(subReply.authorUid)"
:threadAuthorUid="threadAuthorUid"
:replyAuthorUid="replyAuthorUid" />
:threadAuthorUid="thread.authorUid"
:replyAuthorUid="reply.authorUid" />
</RouterLink>
<div class="float-end badge bg-light fs-6 p-1 pe-2" role="group">
<div class="d-inline" :class="{ invisible: hoveringSubReplyID !== subReply.spid }">
<PostCommonMetadataIconLinks :post="subReply" postTypeID="spid" />
</div>
<BadgePostTime :previousPostTime="(subReplyGroup[subReplyGroupIndex - 1] ?? previousSubReplyGroup?.at(-1))?.postedAt"
:currentPostTime="subReply.postedAt"
:nextPostTime="(subReplyGroup[subReplyGroupIndex - 1] ?? nextSubReplyGroup?.[0])?.postedAt"
timestampType="发帖时间" class="bg-info" />
<BadgePostTime postType="楼中楼" :parentPost="reply" :currentPost="subReply"
:previousTimeOverride="getSiblingPostedAt(subReplyGroupIndex, 'previous')"
:nextTimeOverride="getSiblingPostedAt(subReplyGroupIndex, 'next')"
postTimeKey="postedAt" timestampType="发帖时间" class="bg-info" />
</div>
</template>
<div v-viewer.static class="sub-reply-content" v-html="subReply.content" />
Expand All @@ -38,22 +38,26 @@ import type { UserProvision } from './RendererList.vue';
import BadgePostTime from '@/components/Post/badges/BadgePostTime.vue';
import BadgeUser from '@/components/Post/badges/BadgeUser.vue';
import PostCommonMetadataIconLinks from '@/components/Post/badges/PostCommonMetadataIconLinks.vue';
import type { SubReply } from '@/api/post';
import type { BaiduUserID } from '@/api/user';
import type { Reply, SubReply, Thread } from '@/api/post';
import { toUserPortraitImageUrl, toUserRoute } from '@/shared';
import { inject, ref } from 'vue';
import { RouterLink } from 'vue-router';
defineProps<{
const props = defineProps<{
thread: Thread,
reply: Reply,
previousSubReplyGroup?: SubReply[],
subReplyGroup: SubReply[],
nextSubReplyGroup?: SubReply[],
threadAuthorUid: BaiduUserID,
replyAuthorUid: BaiduUserID
nextSubReplyGroup?: SubReply[]
}>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUser, renderUsername } = inject<UserProvision>('userProvision')!;
const hoveringSubReplyID = ref(0);
const getSiblingPostedAt = (index: number, direction: 'previous' | 'next') =>
(props.subReplyGroup[index + (direction === 'next' ? 1 : -1)] as SubReply | undefined
?? (direction === 'next' ? props.nextSubReplyGroup?.[0] : props.previousSubReplyGroup?.at(-1))
)?.postedAt;
</script>

<style scoped>
Expand Down
14 changes: 5 additions & 9 deletions fe/src/components/Post/renderers/list/ThreadItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
<RouterLink :to="{ name: 'post/tid', params: { tid: thread.tid } }"
class="badge bg-light rounded-pill link-dark">只看此帖</RouterLink>
<PostCommonMetadataIconLinks :post="thread" postTypeID="tid" />
<BadgePostTime :previousPostTime="previousThread?.postedAt"
:currentPostTime="thread.postedAt"
:nextPostTime="nextThread?.postedAt"
timestampType="发帖时间" class="bg-success" />
<BadgePostTime postType="主题帖" :previousPost="previousThread" :currentPost="thread" :nextPost="nextThread"
postTimeKey="postedAt" timestampType="发帖时间" class="bg-success" />
</div>
</div>
<div class="row justify-content-between mt-2">
Expand Down Expand Up @@ -70,16 +68,14 @@
<BadgeUser v-if="getUser(thread.latestReplierUid).currentForumModerator !== null"
:user="getUser(thread.latestReplierUid)" class="fs-.75 ms-1" />
</template>
<BadgePostTime :previousPostTime="previousThread?.latestReplyPostedAt"
:currentPostTime="thread.latestReplyPostedAt"
:nextPostTime="nextThread?.latestReplyPostedAt"
timestampType="最后回复时间" class="bg-secondary" />
<BadgePostTime postType="主题帖" :previousPost="previousThread" :currentPost="thread" :nextPost="nextThread"
postTimeKey="latestReplyPostedAt" timestampType="最后回复时间" class="bg-secondary" />
</div>
</div>
</div>
<ReplyItem v-for="(reply, index) in thread.replies" :key="reply.pid"
:previousReply="thread.replies[index - 1]" :reply="reply"
:nextReply="thread.replies[index + 1]" :threadAuthorUid="thread.authorUid" />
:nextReply="thread.replies[index + 1]" :thread="thread" />
</div>
</template>

Expand Down
3 changes: 3 additions & 0 deletions fe/src/shared/fontAwesome.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export {
faChevronUp,
faChevronDown,
faAnglesUp,
faCalendarAlt,
faChartPie,
faClock,
Expand Down
10 changes: 7 additions & 3 deletions fe/src/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ export type ObjValues<T> = T[keyof T];
export type ToPromise<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : never;

export type BootstrapColor = 'danger' | 'dark' | 'info' | 'light' | 'muted' | 'primary' | 'secondary' | 'success' | 'warning';
export type PostType = 'reply' | 'subReply' | 'thread';
export type PostID = typeof postID[number];
export const postType = ['thread', 'reply', 'subReply'] as const;
export type PostType = typeof postType[number];
export const postTypeText = ['主题帖', '回复贴', '楼中楼'] as const;
export type PostTypeText = typeof postTypeText[number];
export const postID = ['tid', 'pid', 'spid'] as const;
export const postTypeToID = { thread: 'tid', reply: 'pid', subReply: 'spid' };
export type PostID = typeof postID[number];
export type Fid = UInt;
export type Tid = UInt;
export type Pid = UInt;
Expand Down Expand Up @@ -65,6 +67,8 @@ export const boolStrToBool = <T>(s: T | 'false' | 'true'): boolean => s === 'tru
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 isElementNode = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE;
export const undefinedOr = <T, TReturn>(value: T | undefined, transformer: (value: T) => TReturn): TReturn | undefined =>
(value === undefined ? undefined : transformer(value));

// https://stackoverflow.com/questions/36532307/rem-px-in-javascript/42769683#42769683
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a#calling-getcomputedstyle
Expand Down

0 comments on commit 79c9887

Please sign in to comment.