Tube - Subscriptions

4 阅读2分钟
- ###### 订阅按钮的展示逻辑
- 通过 `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相关的内容
    • 我们首先确定要添加返回的信息:
      1. 当前浏览者对该视频作者是否订阅?前端会根据这部分内容处理订阅按钮的交互
      2. 计算该视频发布者的订阅者数量
    • 代码逻辑与之前的 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
  }
}