Commit 0a5e9a57 by lijiabin

【需求 25171】feat: 消息通知带 commentId 进详情页时自动定位并高亮对应评论

parent e8108f96
...@@ -22,6 +22,8 @@ import type { ...@@ -22,6 +22,8 @@ import type {
RelatedColumnItemDto, RelatedColumnItemDto,
RelatedColumnParams, RelatedColumnParams,
UpdateArticleShowLatestDto, UpdateArticleShowLatestDto,
LocateCommentParams,
CommentLocationDto,
} from './types' } from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types' import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
import { SpecificVideoRewardEnum, BooleanFlag } from '@/constants' import { SpecificVideoRewardEnum, BooleanFlag } from '@/constants'
...@@ -373,6 +375,17 @@ export const getCommentDetail = (id: number) => { ...@@ -373,6 +375,17 @@ export const getCommentDetail = (id: number) => {
} }
/** /**
* 评论定位:消息通知带 commentId 跳进详情页时,换算出评论在第几页、在哪条楼层/评论下、回复在第几页
*/
export const locateComment = (data: LocateCommentParams) => {
return service.request<CommentLocationDto>({
url: `/api/cultureComment/locateComment`,
method: 'POST',
data,
})
}
/**
* 获取特定视频观看奖励 * 获取特定视频观看奖励
*/ */
export const getSpecificVideoWatchReward = (pageKey: SpecificVideoRewardEnum) => { export const getSpecificVideoWatchReward = (pageKey: SpecificVideoRewardEnum) => {
......
...@@ -417,6 +417,44 @@ export interface CommentChildrenSearchParams extends PageSearchParams { ...@@ -417,6 +417,44 @@ export interface CommentChildrenSearchParams extends PageSearchParams {
} }
/** /**
* 评论定位入参。sortType / size 这些要跟前端列表当前用的保持一致,后端才能算出对得上的页码
*/
export interface LocateCommentParams {
commentId: number
articleId?: number
/** 楼层列表(问吧是回答列表)的排序 */
sortType?: number
/** 问吧专用:回答下面「评论」列表的排序 */
secondSortType?: number
size?: number
secondSize?: number
/** 展开后的回复列表每页条数 */
childSize?: number
}
/**
* 评论定位结果。展示层级:帖子是 楼层 → 回复;问吧是 回答 → 评论 → 回复
*/
export interface CommentLocationDto {
found: boolean
message?: string
targetCommentId: number
articleId: number
rootCommentId: number
rootIndex: number
page: number
/** 问吧专用:目标所在的「评论」id,目标就是回答本身时为空 */
secondCommentId?: number
secondIndex?: number
secondPage?: number
isChild: boolean
childIndex?: number
/** 回复在默认展示的前几条里,不用点「展开」 */
childInPreview?: boolean
childPage?: number
}
/**
* 新增评论 * 新增评论
*/ */
export interface AddCommentDto { export interface AddCommentDto {
......
// 楼层下默认直接展示的回复条数 / 「展开回复」后每页条数,跟后端 getComment、评论定位接口的口径一致
export const PREVIEW_CHILD_SIZE = 5
export const CHILD_PAGE_SIZE = 10
...@@ -192,7 +192,9 @@ ...@@ -192,7 +192,9 @@
v-for="child in getCurrentChildrenList(item)" v-for="child in getCurrentChildrenList(item)"
v-loading="item.loadingChildren" v-loading="item.loadingChildren"
:key="child.id" :key="child.id"
:ref="(el) => setReplyEl(child.id, el)"
class="flex gap-2 p-3 rounded-lg" class="flex gap-2 p-3 rounded-lg"
:class="{ 'reply-locate-highlight': highlightCommentId === child.id }"
> >
<img <img
@click="jumpToUserHomePage({ userId: child.userId, type })" @click="jumpToUserHomePage({ userId: child.userId, type })"
...@@ -276,15 +278,15 @@ ...@@ -276,15 +278,15 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 只有大于5 才会显示这个 --> <!-- 回复超过默认展示条数才会显示这个 -->
<div class="ml-4" v-show="item.childrenNum > 5"> <div class="ml-4" v-show="item.childrenNum > PREVIEW_CHILD_SIZE">
<!-- 展示 展开回复 --> <!-- 展示 展开回复 -->
<button <button
v-show="!item.showChildrenPage" v-show="!item.showChildrenPage"
class="cursor-pointer text-sm text-gray-500" class="cursor-pointer text-sm text-gray-500"
@click="handleExpandReply(item)" @click="handleExpandReply(item)"
> >
还有{{ item.childrenNum - 5 }}条回复,<span 还有{{ item.childrenNum - PREVIEW_CHILD_SIZE }}条回复,<span
class="hover:text-blue-500 transition-colors" class="hover:text-blue-500 transition-colors"
>点击查看</span >点击查看</span
> >
...@@ -292,13 +294,14 @@ ...@@ -292,13 +294,14 @@
<!-- 展示 收起回复 以及分页 --> <!-- 展示 收起回复 以及分页 -->
<div class="flex items-center gap-2" v-show="item.showChildrenPage"> <div class="flex items-center gap-2" v-show="item.showChildrenPage">
<span class="text-sm text-gray-500" <span class="text-sm text-gray-500"
>共{{ Math.ceil(item.childrenNum / 10) }}页</span >共{{ Math.ceil(item.childrenNum / CHILD_PAGE_SIZE) }}页</span
> >
<el-pagination <el-pagination
:pager-count="5" :pager-count="5"
layout="pager" layout="pager"
v-show="item.showChildrenPage" v-show="item.showChildrenPage"
:current-page="item.childrenPageCurrent" :current-page="item.childrenPageCurrent"
:page-size="CHILD_PAGE_SIZE"
:total="item.childrenNum" :total="item.childrenNum"
@current-change="handleChildrenCurrentChange($event, item, index)" @current-change="handleChildrenCurrentChange($event, item, index)"
/> />
...@@ -384,8 +387,9 @@ import { ...@@ -384,8 +387,9 @@ import {
getSecondCommentChildren, getSecondCommentChildren,
topOrCancelTopComment, topOrCancelTopComment,
deleteComment, deleteComment,
locateComment as locateCommentApi,
} from '@/api' } from '@/api'
import { useMessageBox, usePageSearch, useScrollTop } from '@/hooks' import { useMessageBox, usePageSearch, useScrollTop, scrollToLocate } from '@/hooks'
import { ArticleTypeEnum, BooleanFlag } from '@/constants' import { ArticleTypeEnum, BooleanFlag } from '@/constants'
import type { CommentItemDto } from '@/api' import type { CommentItemDto } from '@/api'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
...@@ -396,6 +400,9 @@ import CommentBox from '../CommentBox/index.vue' ...@@ -396,6 +400,9 @@ import CommentBox from '../CommentBox/index.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { push } from 'notivue' import { push } from 'notivue'
import { IS_REAL_KEY_COMMENT, CommentSortTypeEnum } from '@/constants' import { IS_REAL_KEY_COMMENT, CommentSortTypeEnum } from '@/constants'
import type { ComponentPublicInstance } from 'vue'
import type { CommentLocateTarget } from './types'
import { PREVIEW_CHILD_SIZE, CHILD_PAGE_SIZE } from './constants'
const { jumpToUserHomePage } = useNavigation() const { jumpToUserHomePage } = useNavigation()
const { confirm } = useMessageBox() const { confirm } = useMessageBox()
...@@ -722,7 +729,7 @@ const getCommentChildrenList = async (item: CommentItemDto) => { ...@@ -722,7 +729,7 @@ const getCommentChildrenList = async (item: CommentItemDto) => {
pid: item.id, pid: item.id,
articleId: id, articleId: id,
current: item.childrenPageCurrent, current: item.childrenPageCurrent,
size: 10, size: CHILD_PAGE_SIZE,
}) })
item.childrenPageList = data.list.map(formatCommentImageList) item.childrenPageList = data.list.map(formatCommentImageList)
} }
...@@ -736,12 +743,101 @@ const getCurrentChildrenList = (item: CommentItemDto) => { ...@@ -736,12 +743,101 @@ const getCurrentChildrenList = (item: CommentItemDto) => {
} }
} }
// ==================== 评论定位(消息通知带 commentId 跳进详情页) ====================
// 回复的 DOM,定位时滚动用。用函数 ref 收集,不去 querySelector
const replyElMap = new Map<number, HTMLElement>()
const setReplyEl = (replyId: number, el: Element | ComponentPublicInstance | null) => {
if (el instanceof HTMLElement) {
replyElMap.set(replyId, el)
} else {
replyElMap.delete(replyId)
}
}
// 只高亮真正的目标那一条(楼层或者某条回复),不父子一起闪
const flashHighlight = async (commentId: number) => {
highlightCommentId.value = null
await nextTick()
highlightCommentId.value = commentId
setTimeout(() => {
if (highlightCommentId.value === commentId) {
highlightCommentId.value = null
}
}, 2400)
}
/**
* 把本评论区切到目标所在页,目标是被折叠的回复时翻开对应的回复页,然后滚一次、高亮目标。
* 问吧详情页定位到回答下的评论/回复时,也是直接调这个。
*/
const focusTarget = async (target: CommentLocateTarget) => {
syncSearchTarget()
searchParams.value.sortType = target.sortType
searchParams.value.current = target.page
await search()
const index = list.value.findIndex((item: CommentItemDto) => item.id === target.floorId)
const item = list.value[index]
if (!item) {
push.warning('评论位置有变化,没有定位到这条评论')
return
}
const isReply = target.targetId !== target.floorId
if (isReply && !target.childInPreview && target.childPage) {
item.childrenPageCurrent = target.childPage
await handleExpandReply(item)
}
await nextTick()
const floorEl = commentItemRefList.value[index]
if (!floorEl) return
const targetEl = isReply ? replyElMap.get(target.targetId) : floorEl
await scrollToLocate(floorEl, targetEl ?? floorEl)
flashHighlight(targetEl ? target.targetId : target.floorId)
}
/**
* 帖子/视频详情页用:按 commentId 定位。定位失败也要把评论区正常加载出来,不然进来看着像 0 条评论
*/
const locateComment = async (commentId: number) => {
try {
const { data } = await locateCommentApi({
commentId,
articleId: id,
sortType: searchParams.value.sortType,
size: searchParams.value.size,
childSize: CHILD_PAGE_SIZE,
})
if (data?.found) {
await focusTarget({
floorId: data.rootCommentId,
page: data.page,
sortType: searchParams.value.sortType,
targetId: data.targetCommentId,
childInPreview: data.childInPreview,
childPage: data.childPage,
})
return
}
push.warning(data?.message || '没有找到这条评论')
} catch (error) {
// 接口报错时全局拦截器已经弹过提示了
console.error(error)
}
searchParams.value.current = 1
await search()
}
defineExpose({ defineExpose({
scrollToCommentBox: () => handleBackTop(), scrollToCommentBox: () => handleBackTop(),
search: () => { search: () => {
syncSearchTarget() syncSearchTarget()
return search() return search()
}, },
locateComment,
focusTarget,
}) })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
...@@ -825,6 +921,10 @@ defineExpose({ ...@@ -825,6 +921,10 @@ defineExpose({
animation: commentBarFlash 2.4s ease-out; animation: commentBarFlash 2.4s ease-out;
} }
.reply-locate-highlight {
animation: commentFlash 2.4s ease-out;
}
@keyframes commentFlash { @keyframes commentFlash {
0% { 0% {
background-color: rgba(99, 102, 241, 0.16); background-color: rgba(99, 102, 241, 0.16);
......
import type { CommentSortTypeEnum } from '@/constants'
/**
* 评论区自己这一层要定位到哪。
* 帖子/视频的评论区,列表是楼层;问吧回答下内嵌的评论区,列表是回答下面的评论。
*/
export interface CommentLocateTarget {
/** 本评论区列表里要找的那一条 */
floorId: number
/** floorId 在本评论区列表里的页码 */
page: number
/** 算页码时用的排序,定位时会把列表切到这个排序,保证页码对得上 */
sortType: CommentSortTypeEnum
/** 真正要滚动到、高亮的评论:可能是 floorId 本身,也可能是它下面的某条回复 */
targetId: number
/** 目标是回复时:是否在默认展示的前几条里(不用点展开) */
childInPreview?: boolean
/** 目标是回复且不在默认展示里时:在展开后的第几页 */
childPage?: number
}
...@@ -7,3 +7,4 @@ export * from './useNavigation' ...@@ -7,3 +7,4 @@ export * from './useNavigation'
export * from './useMessageBox.tsx' export * from './useMessageBox.tsx'
export * from './useMediaInfo' export * from './useMediaInfo'
export * from './useImagePreview' export * from './useImagePreview'
export * from './useLocateScroll'
\ No newline at end of file
// 顶部吸顶栏的高度,跟 useScrollTop 里的补偿保持一致
const HEADER_OFFSET = 52
const GAP = 12
/**
* 评论定位用的滚动:只滚一次。
*
* 目标是回复时,优先停在它所在楼层(context)的顶部,让楼层和回复在同一屏里都能看到;
* 一屏放不下(楼层内容很长、或者回复排在很后面)才改成让回复停在可视区靠上 1/3 的位置。
* 之前先滚楼层再单独滚回复,第二次滚动会把楼层顶出屏幕,所以这里先算好最终位置再滚。
*/
export const scrollToLocate = (contextEl: HTMLElement, targetEl: HTMLElement = contextEl) => {
const contextTop = contextEl.getBoundingClientRect().top + window.scrollY - HEADER_OFFSET - GAP
let top = contextTop
if (targetEl !== contextEl) {
const rect = targetEl.getBoundingClientRect()
const targetBottom = rect.bottom + window.scrollY
if (targetBottom > contextTop + window.innerHeight - GAP) {
const targetTop = rect.top + window.scrollY
top = targetTop - HEADER_OFFSET - (window.innerHeight - HEADER_OFFSET) / 3
}
}
top = Math.max(0, Math.round(top))
return new Promise<void>((resolve) => {
if (Math.abs(window.scrollY - top) < 2) {
resolve()
return
}
const done = () => {
clearTimeout(timer)
resolve()
}
// 滚动距离太短时浏览器不一定会触发 scrollend,加个超时兜底
const timer = setTimeout(() => {
window.removeEventListener('scrollend', done)
resolve()
}, 1000)
window.addEventListener('scrollend', done, { once: true })
window.scrollTo({ top, behavior: 'smooth' })
})
}
...@@ -35,8 +35,15 @@ export function registerRouterGuards(router: Router) { ...@@ -35,8 +35,15 @@ export function registerRouterGuards(router: Router) {
isCodeLogin: 0, isCodeLogin: 0,
}) })
// } // }
// 只去掉企微授权带回来的 code/state,其余 query 要留着:
// 比如评论通知链接上的 commentId,丢了就定位不到评论了
const query = { ...to.query }
delete query.code
delete query.state
return { return {
path: to.path, path: to.path,
query,
hash: to.hash,
replace: true, replace: true,
} }
} else { } else {
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
v-model:total="articleDetail.replyCount" v-model:total="articleDetail.replyCount"
:type="articleDetail.type" :type="articleDetail.type"
:authorId="articleDetail.createUserId" :authorId="articleDetail.createUserId"
:immediate="!initialCommentId"
/> />
</div> </div>
</div> </div>
...@@ -29,10 +30,13 @@ import ActionButtons from './components/actionButtons.vue' ...@@ -29,10 +30,13 @@ import ActionButtons from './components/actionButtons.vue'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import ArticleContent from '@/components/common/ArticleContent/index.vue' import ArticleContent from '@/components/common/ArticleContent/index.vue'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
const commentRef = useTemplateRef<typeof Comment | null>('commentRef') const commentRef = useTemplateRef<InstanceType<typeof Comment> | null>('commentRef')
const route = useRoute() const route = useRoute()
const id = Number(route.params.id) const id = Number(route.params.id)
// 消息通知链接带的评论 id。点通知都是新开页面重新加载,所以挂载时读一次就够了;
// 有的话评论区不先加载第一页,直接由定位去加载目标所在页
const initialCommentId = Number(route.query.commentId) || 0
const articleDetail = ref({} as ArticleItemDto) const articleDetail = ref({} as ArticleItemDto)
...@@ -45,5 +49,8 @@ const initPage = () => { ...@@ -45,5 +49,8 @@ const initPage = () => {
} }
onMounted(async () => { onMounted(async () => {
initPage() initPage()
if (initialCommentId) {
commentRef.value?.locateComment(initialCommentId)
}
}) })
</script> </script>
...@@ -427,7 +427,7 @@ ...@@ -427,7 +427,7 @@
:ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)" :ref="(e) => (commentRefList[index] = e as InstanceType<typeof Comment>)"
:id="questionId" :id="questionId"
:total="answer.childrenNum" :total="answer.childrenNum"
:defaultSize="5" :defaultSize="ANSWER_COMMENT_PAGE_SIZE"
:type="ArticleTypeEnum.QUESTION" :type="ArticleTypeEnum.QUESTION"
:immediate="false" :immediate="false"
:isQuestion="true" :isQuestion="true"
...@@ -471,9 +471,11 @@ import { ...@@ -471,9 +471,11 @@ import {
addOrCancelCommentLike, addOrCancelCommentLike,
topOrCancelTopComment, topOrCancelTopComment,
deleteComment, deleteComment,
locateComment as locateCommentApi,
} from '@/api' } from '@/api'
import type { ArticleItemDto, CommentItemDto } from '@/api' import type { ArticleItemDto, CommentItemDto, CommentLocationDto } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch, scrollToLocate } from '@/hooks'
import { CHILD_PAGE_SIZE } from '@/components/common/Comment/constants'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import CommentDialog from '@/components/common/CommentDialog/index.vue' import CommentDialog from '@/components/common/CommentDialog/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue' import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
...@@ -537,15 +539,23 @@ const getQuestionDetail = async () => { ...@@ -537,15 +539,23 @@ const getQuestionDetail = async () => {
} }
} }
// 每个回答下面内嵌评论区的每页条数,评论定位算页码时也要用同一个值
const ANSWER_COMMENT_PAGE_SIZE = 5
// 消息通知链接带的评论 id。点通知都是新开页面重新加载,所以挂载时读一次就够了;
// 有的话回答列表不先加载第一页,直接由定位去加载目标所在页
const initialCommentId = Number(route.query.commentId) || 0
const { const {
list, list,
total, total,
searchParams, searchParams,
goToPage, goToPage,
refresh, refresh,
search,
loading: commentLoading, loading: commentLoading,
} = usePageSearch(getCommentList, { } = usePageSearch(getCommentList, {
// immediate: false, immediate: !initialCommentId,
defaultParams: { defaultParams: {
articleId: questionId, articleId: questionId,
sortType: CommentSortTypeEnum.MOST_LIKE, sortType: CommentSortTypeEnum.MOST_LIKE,
...@@ -678,8 +688,86 @@ const openCommentDialog = () => { ...@@ -678,8 +688,86 @@ const openCommentDialog = () => {
commentDialogRef.value?.open(questionDetail.value.id) commentDialogRef.value?.open(questionDetail.value.id)
} }
// ==================== 评论定位(消息通知带 commentId 跳进来) ====================
// 问吧是三层:回答 → 回答下的评论 → 评论下的回复。回答这层由页面自己定位,
// 评论/回复那两层交给该回答内嵌的 Comment 组件(focusTarget),滚动只发生一次
const flashAnswer = async (answerId: number) => {
highlightCommentId.value = null
await nextTick()
highlightCommentId.value = answerId
setTimeout(() => {
if (highlightCommentId.value === answerId) {
highlightCommentId.value = null
}
}, 2400)
}
const locateAnswerComment = async (commentId: number) => {
let result: CommentLocationDto | undefined
try {
const { data } = await locateCommentApi({
commentId,
articleId: questionId,
sortType: searchParams.value.sortType,
size: searchParams.value.size,
// 内嵌评论区每次都按默认的「最高点赞」排,focusTarget 也会把它切回这个排序,页码才对得上
secondSortType: CommentSortTypeEnum.MOST_LIKE,
secondSize: ANSWER_COMMENT_PAGE_SIZE,
childSize: CHILD_PAGE_SIZE,
})
result = data
} catch (error) {
// 接口报错时全局拦截器已经弹过提示了
console.error(error)
}
// 定位不到也要把回答列表正常加载出来,不然进来看着像 0 个回答
if (!result?.found) {
if (result) push.warning(result.message || '没有找到这条评论')
await refresh()
return
}
const location = result
searchParams.value.current = location.page
await search()
const index = list.value.findIndex((answer: CommentItemDto) => answer.id === location.rootCommentId)
const answer = list.value[index]
if (!answer) {
push.warning('评论位置有变化,没有定位到这条评论')
return
}
// 目标就是回答本身
if (!location.secondCommentId) {
await nextTick()
const answerEl = answerRefList.value[index]
if (!answerEl) return
await scrollToLocate(answerEl)
flashAnswer(answer.id)
return
}
// 目标在回答下面:展开该回答的评论区,剩下的交给内嵌评论区
answer.showComment = true
await nextTick()
await commentRefList.value[index]?.focusTarget({
floorId: location.secondCommentId,
page: location.secondPage ?? 1,
sortType: CommentSortTypeEnum.MOST_LIKE,
targetId: location.targetCommentId,
childInPreview: location.childInPreview,
childPage: location.childPage,
})
}
onMounted(() => { onMounted(() => {
getQuestionDetail() getQuestionDetail()
if (initialCommentId) {
locateAnswerComment(initialCommentId)
}
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
......
...@@ -329,6 +329,7 @@ ...@@ -329,6 +329,7 @@
:id="videoId" :id="videoId"
v-model:total="videoDetail.replyCount" v-model:total="videoDetail.replyCount"
:type="ArticleTypeEnum.VIDEO" :type="ArticleTypeEnum.VIDEO"
:immediate="!initialCommentId"
class="mt-5" class="mt-5"
/> />
<RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" /> <RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
...@@ -380,6 +381,9 @@ const rewardFullSetToastRef = useTemplateRef<InstanceType<typeof RewardFullSetTo ...@@ -380,6 +381,9 @@ const rewardFullSetToastRef = useTemplateRef<InstanceType<typeof RewardFullSetTo
'rewardFullSetToastRef', 'rewardFullSetToastRef',
) )
const commentRef = useTemplateRef<InstanceType<typeof Comment> | null>('commentRef') const commentRef = useTemplateRef<InstanceType<typeof Comment> | null>('commentRef')
// 消息通知链接带的评论 id。点通知都是新开页面重新加载,所以挂载时读一次就够了;
// 有的话评论区不先加载第一页,直接由定位去加载目标所在页
const initialCommentId = Number(route.query.commentId) || 0
// 关于视频跳出奖励相关的逻辑 // 加入相关的userId作为区别 不然切换账号的时候 本地存储的一直没变 // 关于视频跳出奖励相关的逻辑 // 加入相关的userId作为区别 不然切换账号的时候 本地存储的一直没变
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
...@@ -580,6 +584,9 @@ const handleShareVideo = async () => { ...@@ -580,6 +584,9 @@ const handleShareVideo = async () => {
} }
onMounted(async () => { onMounted(async () => {
if (initialCommentId) {
commentRef.value?.locateComment(initialCommentId)
}
const { data } = await getArticleDetail(videoId) const { data } = await getArticleDetail(videoId)
videoDetail.value = data videoDetail.value = data
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment