Skip to content

Commit

Permalink
+ providing injected getUser() & renderUsername() with key `userP…
Browse files Browse the repository at this point in the history
…rovision` for its child components

+ exporting type `ThreadWithGroupedSubReplies` for its child components
* move `userRoute()` to `shared/index.ts.toUserRoute()`
@ `<RendererList>`

+ prop `threadAuthorUid`
* invoke `guessReplyContainIntrinsicBlockSize()` in `onMounted()`
@ `<ReplyItem>`

+ prop `(thread|reply)AuthorUid` @ `<SubReplyGroup>`
@ fe
  • Loading branch information
n0099 committed Feb 29, 2024
1 parent 5016218 commit 814c09e
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 33 deletions.
27 changes: 17 additions & 10 deletions fe/src/components/Post/renderers/list/RendererList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,32 @@ import ThreadItem from './ThreadItem.vue';
import { baseGetUser, baseRenderUsername } from '../common';
import type { ApiPosts } from '@/api/index.d';
import type { Reply, SubReply, Thread } from '@/api/post';
import type { BaiduUserID } from '@/api/user';
import { compareRouteIsNewQuery, setComponentCustomScrollBehaviour } from '@/router';
import type { Modify } from '@/shared';
import { convertRemToPixels, isElementNode } from '@/shared';
import { initialTippy } from '@/shared/tippy';
import { computed, nextTick, onMounted } from 'vue';
import { computed, onMounted, provide } from 'vue';
import type { RouterScrollBehavior } from 'vue-router';
import * as _ from 'lodash-es';
const props = defineProps<{ initialPosts: ApiPosts['response'] }>();
const getUser = baseGetUser(props.initialPosts.users);
const renderUsername = baseRenderUsername(getUser);
const userRoute = (uid: BaiduUserID) => ({ name: 'user/uid', params: { uid } });
const userProvision = { getUser, renderUsername };
// export type UserProvision = typeof userProvision;
// will trigger @typescript-eslint/no-unsafe-assignment when `inject<UserProvision>('userProvision')`
export interface UserProvision {
getUser: ReturnType<typeof baseGetUser>,
renderUsername: ReturnType<typeof baseRenderUsername>
}
provide<UserProvision>('userProvision', userProvision);
export type ThreadWithGroupedSubReplies<AdditionalSubReply = never> =
Thread & { replies: Array<Reply & { subReplies: Array<AdditionalSubReply | SubReply[]> }> };
const posts = computed(() => {
const newPosts = _.cloneDeep(props.initialPosts) as Modify<ApiPosts['response'], { // https://github.com/microsoft/TypeScript/issues/33591
threads: Array<Thread & { replies: Array<Reply & { subReplies: Array<SubReply | SubReply[]> }> }>
}>;
// https://github.com/microsoft/TypeScript/issues/33591
const newPosts = _.cloneDeep(props.initialPosts) as
Modify<ApiPosts['response'], { threads: Array<ThreadWithGroupedSubReplies<SubReply>> }>;
newPosts.threads = newPosts.threads.map(thread => {
thread.replies = thread.replies.map(reply => {
// eslint-disable-next-line unicorn/no-array-reduce
Expand Down Expand Up @@ -55,9 +64,7 @@ const posts = computed(() => {
return thread;
});
return newPosts as Modify<ApiPosts['response'], {
threads: Array<Thread & { replies: Array<Reply & { subReplies: SubReply[][] }> }>
}>;
return newPosts as Modify<ApiPosts['response'], { threads: ThreadWithGroupedSubReplies[] }>;
});
onMounted(initialTippy);
Expand Down
28 changes: 19 additions & 9 deletions fe/src/components/Post/renderers/list/ReplyItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,52 @@
</div>
</div>
<div :ref="el => el !== null && replyElements.push(el as HTMLElement)"
class="reply row shadow-sm bs-callout bs-callout-info">
class="reply row shadow-sm bs-callout bs-callout-info">
<div v-for="author in [getUser(reply.authorUid)]" :key="author.uid"
class="reply-author col-auto text-center sticky-top shadow-sm badge bg-light">
<RouterLink :to="userRoute(author.uid)" class="d-block">
<RouterLink :to="toUserRoute(author.uid)" class="d-block">
<img :src="toUserPortraitImageUrl(author.portrait)" loading="lazy" class="tieba-user-portrait-large" />
<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="thread.authorUid" />
<BadgeUser :user="getUser(reply.authorUid)" :threadAuthorUid="threadAuthorUid" />
</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" />

Check warning on line 36 in fe/src/components/Post/renderers/list/ReplyItem.vue

View workflow job for this annotation

GitHub Actions / eslint

'v-html' directive can lead to XSS attack
<template v-if="reply.subReplies.length > 0">
<SubReplyGroup v-for="(subReplyGroup, _k) in reply.subReplies" :key="_k" :subReplyGroup="subReplyGroup" />
<SubReplyGroup v-for="(subReplyGroup, _k) in reply.subReplies" :key="_k" :subReplyGroup="subReplyGroup"
:threadAuthorUid="threadAuthorUid" :replyAuthorUid="reply.authorUid" />
</template>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import type { ThreadWithGroupedSubReplies, UserProvision } from './RendererList.vue';
import { guessReplyContainIntrinsicBlockSize } from './index';
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 { Reply } from '@/api/post';
import { toUserPortraitImageUrl } from '@/shared';
import type { BaiduUserID } from '@/api/user';
import { toUserPortraitImageUrl, toUserRoute } from '@/shared';
import { useElementRefsStore } from '@/stores/elementRefs';
import '@/styles/bootstrapCallout.css';
import { ref } from 'vue';
import { inject, nextTick, onMounted, ref } from 'vue';
import { RouterLink } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
defineProps<{ reply: Reply }>();
defineProps<{ reply: ThreadWithGroupedSubReplies['replies'][number], threadAuthorUid: BaiduUserID }>();
const elementRefsStore = useElementRefsStore();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUser } = inject<UserProvision>('userProvision')!;
const replyElements = ref<HTMLElement[]>([]);
onMounted(async () => {
await nextTick();
guessReplyContainIntrinsicBlockSize(replyElements.value);
});
</script>

<style scoped>
Expand Down Expand Up @@ -87,4 +97,4 @@ const replyElements = ref<HTMLElement[]>([]);
font-size: 1rem;
line-height: 150%;
}
</style>
</style>
18 changes: 11 additions & 7 deletions fe/src/components/Post/renderers/list/SubReplyGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
@mouseleave="() => { hoveringSubReplyID = 0 }"
class="sub-reply-item list-group-item">
<template v-for="author in [getUser(subReply.authorUid)]" :key="author.uid">
<RouterLink v-if="subReplyGroup[subReplyIndex - 1] === undefined" :to="userRoute(author.uid)"
<RouterLink v-if="subReplyGroup[subReplyIndex - 1] === undefined" :to="toUserRoute(author.uid)"
class="sub-reply-author text-wrap badge bg-light">
<img :src="toUserPortraitImageUrl(author.portrait)"
loading="lazy" class="tieba-user-portrait-small" />
<span class="mx-2 align-middle link-dark">
{{ renderUsername(subReply.authorUid) }}
</span>
<BadgeUser :user="getUser(subReply.authorUid)"
:threadAuthorUid="thread.authorUid"
:replyAuthorUid="reply.authorUid" />
:threadAuthorUid="threadAuthorUid"
:replyAuthorUid="replyAuthorUid" />
</RouterLink>
<div class="float-end badge bg-light">
<div class="d-inline" :class="{ invisible: hoveringSubReplyID !== subReply.spid }">
Expand All @@ -32,15 +32,19 @@
</template>

<script setup lang="ts">
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 { toUserPortraitImageUrl } from '@/shared';
import { ref } from 'vue';
import type { BaiduUserID } from '@/api/user';
import { toUserPortraitImageUrl, toUserRoute } from '@/shared';
import { inject, ref } from 'vue';
import { RouterLink } from 'vue-router';
defineProps<{ subReplyGroup: SubReply[] }>();
defineProps<{ subReplyGroup: SubReply[], threadAuthorUid: BaiduUserID, replyAuthorUid: BaiduUserID }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUser, renderUsername } = inject<UserProvision>('userProvision')!;
const hoveringSubReplyID = ref(0);
</script>

Expand All @@ -59,4 +63,4 @@ const hoveringSubReplyID = ref(0);
.sub-reply-author {
font-size: .9rem;
}
</style>
</style>
16 changes: 10 additions & 6 deletions fe/src/components/Post/renderers/list/ThreadItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
</span>
</div>
<div class="col-auto badge bg-light" role="group">
<RouterLink :to="userRoute(thread.authorUid)">
<RouterLink :to="toUserRoute(thread.authorUid)">
<span v-if="thread.latestReplierUid !== thread.authorUid"
class="fw-normal link-success">楼主:</span>
<span v-else class="fw-normal link-info">楼主兼最后回复:</span>
Expand All @@ -58,7 +58,7 @@
<span class="fw-bold link-dark">未知用户</span>
</template>
<template v-else-if="thread.latestReplierUid !== thread.authorUid">
<RouterLink :to="userRoute(thread.latestReplierUid)" class="ms-2">
<RouterLink :to="toUserRoute(thread.latestReplierUid)" class="ms-2">
<span class="fw-normal link-secondary">最后回复:</span>
<span class="fw-bold link-dark">{{ renderUsername(thread.latestReplierUid) }}</span>
</RouterLink>
Expand All @@ -67,24 +67,28 @@
</div>
</div>
</div>
<ReplyItem v-for="reply in thread.replies" :key="reply.pid" :reply="reply" />
<ReplyItem v-for="reply in thread.replies" :key="reply.pid" :reply="reply" :threadAuthorUid="thread.authorUid" />
</div>
</template>

<script setup lang="ts">
import type { ThreadWithGroupedSubReplies, UserProvision } from './RendererList.vue';
import ReplyItem from './ReplyItem.vue';
import BadgePostTime from '@/components/Post/badges/BadgePostTime.vue';
import BadgeThread from '@/components/Post/badges/BadgeThread.vue';
import BadgeUser from '@/components/Post/badges/BadgeUser.vue';
import PostCommonMetadataIconLinks from '@/components/Post/badges/PostCommonMetadataIconLinks.vue';
import type { Thread } from '@/api/post';
import { toUserRoute } from '@/shared/index';
import { useElementRefsStore } from '@/stores/elementRefs';
import { inject } from 'vue';
import { RouterLink } from 'vue-router';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { DateTime } from 'luxon';
defineProps<{ thread: Thread }>();
defineProps<{ thread: ThreadWithGroupedSubReplies }>();
const elementRefsStore = useElementRefsStore();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUser, renderUsername } = inject<UserProvision>('userProvision')!;
</script>

<style scoped>
Expand All @@ -104,4 +108,4 @@ const elementRefsStore = useElementRefsStore();
flex-basis: 100%;
inline-size: 0;
}
</style>
</style>
5 changes: 4 additions & 1 deletion fe/src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Cursor } from '@/api/index.d';
import type { User } from '@/api/user';
import type { BaiduUserID, User } from '@/api/user';
import { computed } from 'vue';
import type { LocationAsRelativeRaw } from 'vue-router';
import Noty from 'noty';
import * as _ from 'lodash-es';

Expand Down Expand Up @@ -51,6 +52,8 @@ export const toUserProfileUrl = (user: Partial<Pick<User, 'name' | 'portrait'>>)
: `https://tieba.baidu.com/home/main?id=${user.portrait}`);
export const toUserPortraitImageUrl = (portrait: string) =>
`https://himg.bdimg.com/sys/portrait/item/${portrait}.jpg`; // use /sys/portraith for high-res image
export const toUserRoute = (uid: BaiduUserID): LocationAsRelativeRaw =>
({ name: 'user/uid', params: { uid: uid.toString() } });

export const removeStart = (s: string, remove: string) => (s.startsWith(remove) ? s.slice(remove.length) : s);
export const removeEnd = (s: string, remove: string) => (s.endsWith(remove) ? s.slice(0, -remove.length) : s);
Expand Down

0 comments on commit 814c09e

Please sign in to comment.