From a882f98d9102b8f0a1c6af15465adc0d6af672f4 Mon Sep 17 00:00:00 2001 From: dean Date: Fri, 7 Nov 2025 12:16:01 -0800 Subject: [PATCH 1/3] Add home option to VideoFeedInputGQL The home feed option enables smart feed selection on the backend: - If user has following: returns FOLLOWING feed - If user has no following: returns ALL feed - Always includes hasFollowing flag so frontend knows which feed it got This allows frontend to make a single query instead of needing to check following status separately. --- src/index.tsx | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/schema.gql | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 92b7f38..529ede5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2286,6 +2286,7 @@ export type Mutation = { createSubscription: CreateSubscriptionResultGql; createUploadStream: CreateUploadStreamReturn; deleteComment: Scalars["Boolean"]["output"]; + deleteNotification: Scalars["Boolean"]["output"]; deleteTags: Scalars["Boolean"]["output"]; deleteUser: Scalars["Boolean"]["output"]; deleteVideo: Scalars["Boolean"]["output"]; @@ -2300,6 +2301,9 @@ export type Mutation = { getHlsInitUploadLink: GetUploadLinkReturn; getProfileImageUploadLink: GetProfileUploadLinkReturn; getUploadLink: GetUploadLinkReturn; + markAllNotificationsAsRead: Scalars["Boolean"]["output"]; + markNotificationAsRead: Scalars["Boolean"]["output"]; + markNotificationsAsRead: Scalars["Boolean"]["output"]; reactToVideo: Scalars["Boolean"]["output"]; reportContent: Scalars["Boolean"]["output"]; retireTags: Scalars["Boolean"]["output"]; @@ -2346,6 +2350,10 @@ export type MutationDeleteCommentArgs = { videoId: Scalars["Int"]["input"]; }; +export type MutationDeleteNotificationArgs = { + notificationId: Scalars["Int"]["input"]; +}; + export type MutationDeleteTagsArgs = { tagsToDelete: Array; videoId: Scalars["Int"]["input"]; @@ -2401,6 +2409,14 @@ export type MutationGetUploadLinkArgs = { videoId: Scalars["Int"]["input"]; }; +export type MutationMarkNotificationAsReadArgs = { + notificationId: Scalars["Int"]["input"]; +}; + +export type MutationMarkNotificationsAsReadArgs = { + notificationIds: Array; +}; + export type MutationReactToVideoArgs = { reaction?: InputMaybe; videoId: Scalars["Int"]["input"]; @@ -2441,6 +2457,39 @@ export type NoInitForChunkedUploadErr = { segmentType: StreamSegmentTypeEnum; }; +export type NotificationConnection = { + __typename?: "NotificationConnection"; + hasMore: Scalars["Boolean"]["output"]; + notifications: Array; + totalCount: Scalars["Int"]["output"]; + unreadCount: Scalars["Int"]["output"]; +}; + +export type NotificationFilters = { + isRead?: InputMaybe; + notificationTypes?: InputMaybe>; +}; + +export type NotificationGql = { + __typename?: "NotificationGQL"; + actor: UserGql; + comment?: Maybe; + createdAt: Scalars["DateTime"]["output"]; + id: Scalars["Int"]["output"]; + isRead: Scalars["Boolean"]["output"]; + notificationType: NotificationTypeEnum; + reactionType?: Maybe; + readAt?: Maybe; + videoId?: Maybe; +}; + +export enum NotificationTypeEnum { + Comment = "COMMENT", + CommentReply = "COMMENT_REPLY", + Follow = "FOLLOW", + Reaction = "REACTION", +} + export type OtherErrorNeedsNote = { __typename?: "OtherErrorNeedsNote"; msg?: Maybe; @@ -2550,6 +2599,8 @@ export type Query = { getVideo: VideoGql; getVideoMakePercentageIntervals: Array; getVideos: Array; + notifications: NotificationConnection; + unreadNotificationCount: Scalars["Int"]["output"]; waitFor: Scalars["Float"]["output"]; }; @@ -2689,6 +2740,12 @@ export type QueryGetVideosArgs = { videoIds: Array; }; +export type QueryNotificationsArgs = { + filters?: InputMaybe; + limit?: Scalars["Int"]["input"]; + offset?: Scalars["Int"]["input"]; +}; + export type QueryWaitForArgs = { duration: Scalars["Float"]["input"]; }; @@ -3170,16 +3227,25 @@ export type VideoFeedInputGql = | { allUsers: Scalars["Boolean"]["input"]; followedByUserId?: never; + home?: never; userId?: never; } | { allUsers?: never; followedByUserId: Scalars["Int"]["input"]; + home?: never; userId?: never; } | { allUsers?: never; followedByUserId?: never; + home: Scalars["Boolean"]["input"]; + userId?: never; + } + | { + allUsers?: never; + followedByUserId?: never; + home?: never; userId: Scalars["Int"]["input"]; }; diff --git a/src/schema.gql b/src/schema.gql index 9ca10fe..715cf0d 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -28,6 +28,12 @@ type Query { when: DateTime = null ): CountLeaderboardGQL! getMedals(scope: MedalScope!, userId: Int = null): RequestedMedalsGQL! + notifications( + limit: Int! = 20 + offset: Int! = 0 + filters: NotificationFilters = null + ): NotificationConnection! + unreadNotificationCount: Int! getRuns( filterInput: RunFilterInput! runIds: [Int!] = null @@ -647,6 +653,7 @@ input VideoFeedInputGQL @oneOf { followedByUserId: Int userId: Int allUsers: Boolean + home: Boolean } type MakePercentageIntervalGQL { @@ -715,6 +722,37 @@ input MedalScope @oneOf { datetimeRange: DatetimeRangeAggregationInput } +type NotificationConnection { + notifications: [NotificationGQL!]! + totalCount: Int! + unreadCount: Int! + hasMore: Boolean! +} + +type NotificationGQL { + id: Int! + notificationType: NotificationTypeEnum! + actor: UserGQL! + videoId: Int + comment: CommentGQL + reactionType: String + isRead: Boolean! + createdAt: DateTime! + readAt: DateTime +} + +enum NotificationTypeEnum { + COMMENT + COMMENT_REPLY + REACTION + FOLLOW +} + +input NotificationFilters { + isRead: Boolean = null + notificationTypes: [NotificationTypeEnum!] = null +} + type GetRunsResult { runs: [RunGQL!]! count: Int @@ -916,6 +954,10 @@ type Mutation { reason: ReportReasonEnum! customReason: String = null ): Boolean! + markNotificationAsRead(notificationId: Int!): Boolean! + markAllNotificationsAsRead: Boolean! + markNotificationsAsRead(notificationIds: [Int!]!): Boolean! + deleteNotification(notificationId: Int!): Boolean! addAnnotationToShot( shotId: Int! annotationName: String! -- 2.49.1 From 83b7f0d0f6ebfda613ce98cebd984c5bcc917449 Mon Sep 17 00:00:00 2001 From: dean Date: Sat, 8 Nov 2025 09:15:39 -0800 Subject: [PATCH 2/3] Add home feed and isFollowedByCurrentUser field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add home option to VideoFeedInputGQL for automatic feed selection - Add hasFollowing field to VideoHistoryGQL response - Add isFollowedByCurrentUser field to UserGQL to replace followers N+1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/index.tsx | 1 + src/schema.gql | 1 + 2 files changed, 2 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 529ede5..c517dbb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3178,6 +3178,7 @@ export type UserGql = { following?: Maybe>; id: Scalars["Int"]["output"]; isAdmin?: Maybe; + isFollowedByCurrentUser?: Maybe; profileImageUri?: Maybe; stripeCustomerId?: Maybe; updatedAt?: Maybe; diff --git a/src/schema.gql b/src/schema.gql index 715cf0d..1db3355 100644 --- a/src/schema.gql +++ b/src/schema.gql @@ -374,6 +374,7 @@ type UserGQL { agreesToMarketing: Boolean following: [UserGQL!] followers: [UserGQL!] + isFollowedByCurrentUser: Boolean } type ShotGQL { -- 2.49.1 From e4223a1a1743ab9935bad19cbe8bcb822c59ac3c Mon Sep 17 00:00:00 2001 From: dean Date: Sun, 9 Nov 2025 23:44:39 -0800 Subject: [PATCH 3/3] Add isFollowedByCurrentUser field and hasFollowing to feed query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated GraphQL operations to support efficient follow status checking: - Added isFollowedByCurrentUser field to UserSocialsFields fragment - Removed nested followers array from UserSocialsFields (over-fetching) - Simplified followUser/unfollowUser mutations to return minimal data - Added hasFollowing field to GetVideoFeed query for feed mode detection - Updated getUserFollowingFollowers query to include isFollowedByCurrentUser These changes enable the mobile app to: - Display correct follow/unfollow button states without client-side lookups - Differentiate between "Following" and "Popular" feed modes - Reduce payload size by removing unnecessary nested data Backend handles efficient resolution via request-scoped caching. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/index.tsx | 155 ++++++--------------------------------- src/operations/feed.gql | 7 +- src/operations/user.gql | 22 +----- src/operations/video.gql | 6 +- 4 files changed, 31 insertions(+), 159 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index c517dbb..43595cb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3546,12 +3546,7 @@ export type GetFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; comments: Array<{ @@ -3563,12 +3558,7 @@ export type GetFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; replies: Array<{ __typename?: "CommentGQL"; @@ -3579,12 +3569,7 @@ export type GetFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; }>; @@ -3602,12 +3587,7 @@ export type UserSocialsFieldsFragment = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; export type VideoCardFieldsFragment = { @@ -3664,12 +3644,7 @@ export type VideoCardFieldsFragment = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; comments: Array<{ @@ -3681,12 +3656,7 @@ export type VideoCardFieldsFragment = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; replies: Array<{ __typename?: "CommentGQL"; @@ -3697,12 +3667,7 @@ export type VideoCardFieldsFragment = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; }>; @@ -3721,6 +3686,7 @@ export type GetVideoFeedQuery = { __typename?: "Query"; getFeedVideos: { __typename?: "VideoHistoryGQL"; + hasFollowing: boolean; videos: Array<{ __typename?: "VideoGQL"; id: number; @@ -3778,12 +3744,7 @@ export type GetVideoFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; comments: Array<{ @@ -3795,12 +3756,7 @@ export type GetVideoFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; replies: Array<{ __typename?: "CommentGQL"; @@ -3811,12 +3767,7 @@ export type GetVideoFeedQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; }>; @@ -4847,21 +4798,7 @@ export type FollowUserMutationVariables = Exact<{ export type FollowUserMutation = { __typename?: "Mutation"; - followUser: { - __typename?: "UserGQL"; - username: string; - id: number; - following?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - }> | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - }> | null; - }; + followUser: { __typename?: "UserGQL"; id: number; username: string }; }; export type UnfollowUserMutationVariables = Exact<{ @@ -4870,21 +4807,7 @@ export type UnfollowUserMutationVariables = Exact<{ export type UnfollowUserMutation = { __typename?: "Mutation"; - unfollowUser: { - __typename?: "UserGQL"; - username: string; - id: number; - following?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - }> | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - }> | null; - }; + unfollowUser: { __typename?: "UserGQL"; id: number; username: string }; }; export type GetUserFollowingFollowersQueryVariables = Exact<{ @@ -4901,12 +4824,14 @@ export type GetUserFollowingFollowersQuery = { id: number; username: string; profileImageUri?: string | null; + isFollowedByCurrentUser?: boolean | null; }> | null; followers?: Array<{ __typename?: "UserGQL"; id: number; username: string; profileImageUri?: string | null; + isFollowedByCurrentUser?: boolean | null; }> | null; } | null; }; @@ -5129,12 +5054,7 @@ export type GetVideoSocialDetailsByIdQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; comments: Array<{ @@ -5146,12 +5066,7 @@ export type GetVideoSocialDetailsByIdQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; replies: Array<{ __typename?: "CommentGQL"; @@ -5162,12 +5077,7 @@ export type GetVideoSocialDetailsByIdQuery = { id: number; username: string; profileImageUri?: string | null; - followers?: Array<{ - __typename?: "UserGQL"; - id: number; - username: string; - profileImageUri?: string | null; - }> | null; + isFollowedByCurrentUser?: boolean | null; }; }>; }>; @@ -5755,11 +5665,7 @@ export const UserSocialsFieldsFragmentDoc = gql` id username profileImageUri - followers { - id - username - profileImageUri - } + isFollowedByCurrentUser } `; export const VideoCardFieldsFragmentDoc = gql` @@ -6636,6 +6542,7 @@ export const GetVideoFeedDocument = gql` hasNextPage endCursor } + hasFollowing } } ${VideoCardFieldsFragmentDoc} @@ -9063,16 +8970,8 @@ export type GetUserTagsQueryResult = Apollo.QueryResult< export const FollowUserDocument = gql` mutation followUser($followedUserId: Int!) { followUser(followedUserId: $followedUserId) { - username id - following { - id - username - } - followers { - id - username - } + username } } `; @@ -9122,16 +9021,8 @@ export type FollowUserMutationOptions = Apollo.BaseMutationOptions< export const UnfollowUserDocument = gql` mutation unfollowUser($followedUserId: Int!) { unfollowUser(followedUserId: $followedUserId) { - username id - following { - id - username - } - followers { - id - username - } + username } } `; @@ -9186,11 +9077,13 @@ export const GetUserFollowingFollowersDocument = gql` id username profileImageUri + isFollowedByCurrentUser } followers { id username profileImageUri + isFollowedByCurrentUser } } } diff --git a/src/operations/feed.gql b/src/operations/feed.gql index 39baaa8..e0f271f 100644 --- a/src/operations/feed.gql +++ b/src/operations/feed.gql @@ -19,11 +19,7 @@ fragment UserSocialsFields on UserGQL { id username profileImageUri - followers { - id - username - profileImageUri - } + isFollowedByCurrentUser } fragment VideoCardFields on VideoGQL { @@ -116,5 +112,6 @@ query GetVideoFeed( hasNextPage endCursor } + hasFollowing } } diff --git a/src/operations/user.gql b/src/operations/user.gql index b0fd753..557bc27 100644 --- a/src/operations/user.gql +++ b/src/operations/user.gql @@ -88,31 +88,15 @@ query GetUserTags { mutation followUser($followedUserId: Int!) { followUser(followedUserId: $followedUserId) { - username id - following { - id - username - } - followers { - id - username - } + username } } mutation unfollowUser($followedUserId: Int!) { unfollowUser(followedUserId: $followedUserId) { - username id - following { - id - username - } - followers { - id - username - } + username } } @@ -123,11 +107,13 @@ query getUserFollowingFollowers { id username profileImageUri + isFollowedByCurrentUser } followers { id username profileImageUri + isFollowedByCurrentUser } } } diff --git a/src/operations/video.gql b/src/operations/video.gql index f76405f..e3392e3 100644 --- a/src/operations/video.gql +++ b/src/operations/video.gql @@ -86,11 +86,7 @@ fragment UserSocialsFields on UserGQL { id username profileImageUri - followers { - id - username - profileImageUri - } + isFollowedByCurrentUser } query GetVideoSocialDetailsById($videoId: Int!) { -- 2.49.1