- ###### 订阅按钮的展示逻辑
- 通过 `useAuth()` 获取当前登录用户的`userId`,与视频信息中存储的`user.clerkId`比较,判断当前登录用户是否是视频的发布者
- 视频发布者浏览自己发布的视频时看到的是 "Edit Video"按钮
- 只有在浏览非自己发布的视频时才会展示订阅相关的按钮,根据订阅状态判断是“Subscribe”还是“Unsubscribe”
-
Schema
- 同表关联冲突:在一张表中有2个字段都引用了同一个外键目标表(这里是users表),就会遇到同表关联冲突
- 我们通过
realationName 解决同表关联冲突
// video subscriptions
export const subscriptions = pgTable("subscriptions", {
viewerId: uuid("viewer_id").references(() => users.id, {
onDelete: "cascade",
}).notNull(), // 订阅者
creatorId: uuid("creator_id").references(() => users.id, {
onDelete: "cascade",
}).notNull(), // 被订阅的创作者
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("update_at").defaultNow().notNull(),
}, t => [ primaryKey({ name: "subscriptions_p_key", columns: [t.viewerId, t.creatorId],
})
])
export const subscriptionRelations = relations(subscriptions, ({ one }) => ({
viewer: one(users, {
fields: [subscriptions.viewerId],
references: [users.id],
relationName: 'subscriptions_viewer_id_f_key' // 解决同表关联冲突
}),
creator: one(users, {
fields: [subscriptions.creatorId],
references: [users.id],
relationName: 'subscriptions_creator_id_f_key' // 解决同表关联冲突
}),
}))
export const userRelations = relations(users, ({ many }) => ({
...
subscriptions: many(subscriptions, {
relationName: 'subscriptions_viewer_id_f_key'
}),
subscribers: many(subscriptions, {
relationName: 'subscriptions_creator_id_f_key'
}),
}))
-
在获取视频详细信息的接口中添加subscription相关的内容
- 我们首先确定要添加返回的信息:
- 当前浏览者对该视频作者是否订阅?前端会根据这部分内容处理订阅按钮的交互
- 计算该视频发布者的订阅者数量
- 代码逻辑与之前的 Video Reactions 部分基本相同,首先
db.$with()创建一个子查询,获取当前浏览者的所有订阅记录作为一张临时表,再通过leftJoin()关联视频浏览者与当前视频创作者的订阅记录
- 需要注意的是
isNotNull() 是一个 conditional helper function,目前这种函数不能在query select中使用,因为Drizzle无法在Ts中推断他的返回类型(会是unknown),所以我们这里使用mapWith(Boolean)
const viewerSubscriptions = db.$with('viewer_subscriptions').as(
db
.select()
.from(subscriptions)
.where(inArray(subscriptions.viewerId, viewerId ? [viewerId] : []))
)
const [video] = await db
.with(viewerReactions) // 使用上面定义的子查询
.select({
// 视频发布者的信息
user: {
...getTableColumns(users),
// 计算该视频发布者的订阅者数量
subscriberCount: db.$count(subscriptions, eq(subscriptions.creatorId, users.id)),
// 当前浏览视频的用户是否订阅了该视频的发布者
viewerIsSubscribed: isNotNull(viewerSubscriptions.viewerId).mapWith(Boolean)
},
})
.from(videos)
.innerJoin(...),
.leftJoin(...),
.leftJoin(viewerSubscriptions, eq(users.id, viewerSubscriptions.creatorId)) // 关联当前浏览视频的用户对当前视频创作者的订阅记录
...
-
自定义Hook useSubscription()
- 我们将订阅按钮的展示需要的状态都放在hook里统一处理,以便在不同的地方的使用订阅按钮时可复用
interface UseSubscriptionProps {
userId: string;
isSubscribed: boolean;
fromVideoId?: string;
}
export const useSubscription = ({
userId,
isSubscribed,
fromVideoId,
}: UseSubscriptionProps) => {
const clerk = useClerk();
const utils = trpc.useUtils();
const subscribe = trpc.subscriptions.create.useMutation({
onSuccess: () => {
toast.success('Subscribed')
if(fromVideoId) {
utils.videos.getOne.invalidate({ videoId: fromVideoId })
}
},
onError: (error) => {
toast.error('Something went wrong')
if(error.data?.code === 'UNAUTHORIZED') {
clerk.openSignIn()
}
}
})
const unsubscribe = trpc.subscriptions.remove.useMutation({
onSuccess: () => {
toast.success('UnSubscribed')
if(fromVideoId) {
utils.videos.getOne.invalidate({ videoId: fromVideoId })
}
},
onError: (error) => {
toast.error('Something went wrong')
if(error.data?.code === 'UNAUTHORIZED') {
clerk.openSignIn()
}
}
})
const isSubscribePending = subscribe.isPending || unsubscribe.isPending;
const handleSubscribeClick = () => {
if( isSubscribed ) {
unsubscribe.mutate({ userId })
}else {
subscribe.mutate({ userId })
}
}
return {
isSubscribePending,
handleSubscribeClick
}
}