Compare commits

..

3 Commits

Author SHA1 Message Date
dean
e4223a1a17 Add isFollowedByCurrentUser field and hasFollowing to feed query
All checks were successful
Tests / Tests (pull_request) Successful in 9s
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 <noreply@anthropic.com>
2025-11-09 23:44:39 -08:00
dean
83b7f0d0f6 Add home feed and isFollowedByCurrentUser field
- 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 <noreply@anthropic.com>
2025-11-08 09:15:39 -08:00
dean
a882f98d91 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.
2025-11-07 12:16:01 -08:00
5 changed files with 141 additions and 159 deletions

View File

@@ -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<VideoTagInput>;
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<Scalars["Int"]["input"]>;
};
export type MutationReactToVideoArgs = {
reaction?: InputMaybe<ReactionEnum>;
videoId: Scalars["Int"]["input"];
@@ -2441,6 +2457,39 @@ export type NoInitForChunkedUploadErr = {
segmentType: StreamSegmentTypeEnum;
};
export type NotificationConnection = {
__typename?: "NotificationConnection";
hasMore: Scalars["Boolean"]["output"];
notifications: Array<NotificationGql>;
totalCount: Scalars["Int"]["output"];
unreadCount: Scalars["Int"]["output"];
};
export type NotificationFilters = {
isRead?: InputMaybe<Scalars["Boolean"]["input"]>;
notificationTypes?: InputMaybe<Array<NotificationTypeEnum>>;
};
export type NotificationGql = {
__typename?: "NotificationGQL";
actor: UserGql;
comment?: Maybe<CommentGql>;
createdAt: Scalars["DateTime"]["output"];
id: Scalars["Int"]["output"];
isRead: Scalars["Boolean"]["output"];
notificationType: NotificationTypeEnum;
reactionType?: Maybe<Scalars["String"]["output"]>;
readAt?: Maybe<Scalars["DateTime"]["output"]>;
videoId?: Maybe<Scalars["Int"]["output"]>;
};
export enum NotificationTypeEnum {
Comment = "COMMENT",
CommentReply = "COMMENT_REPLY",
Follow = "FOLLOW",
Reaction = "REACTION",
}
export type OtherErrorNeedsNote = {
__typename?: "OtherErrorNeedsNote";
msg?: Maybe<Scalars["String"]["output"]>;
@@ -2550,6 +2599,8 @@ export type Query = {
getVideo: VideoGql;
getVideoMakePercentageIntervals: Array<MakePercentageIntervalGql>;
getVideos: Array<VideoGql>;
notifications: NotificationConnection;
unreadNotificationCount: Scalars["Int"]["output"];
waitFor: Scalars["Float"]["output"];
};
@@ -2689,6 +2740,12 @@ export type QueryGetVideosArgs = {
videoIds: Array<Scalars["Int"]["input"]>;
};
export type QueryNotificationsArgs = {
filters?: InputMaybe<NotificationFilters>;
limit?: Scalars["Int"]["input"];
offset?: Scalars["Int"]["input"];
};
export type QueryWaitForArgs = {
duration: Scalars["Float"]["input"];
};
@@ -3121,6 +3178,7 @@ export type UserGql = {
following?: Maybe<Array<UserGql>>;
id: Scalars["Int"]["output"];
isAdmin?: Maybe<Scalars["Boolean"]["output"]>;
isFollowedByCurrentUser?: Maybe<Scalars["Boolean"]["output"]>;
profileImageUri?: Maybe<Scalars["String"]["output"]>;
stripeCustomerId?: Maybe<Scalars["String"]["output"]>;
updatedAt?: Maybe<Scalars["DateTime"]["output"]>;
@@ -3170,16 +3228,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"];
};
@@ -3479,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<{
@@ -3496,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";
@@ -3512,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;
};
}>;
}>;
@@ -3535,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 = {
@@ -3597,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<{
@@ -3614,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";
@@ -3630,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;
};
}>;
}>;
@@ -3654,6 +3686,7 @@ export type GetVideoFeedQuery = {
__typename?: "Query";
getFeedVideos: {
__typename?: "VideoHistoryGQL";
hasFollowing: boolean;
videos: Array<{
__typename?: "VideoGQL";
id: number;
@@ -3711,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<{
@@ -3728,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";
@@ -3744,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;
};
}>;
}>;
@@ -4780,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<{
@@ -4803,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<{
@@ -4834,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;
};
@@ -5062,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<{
@@ -5079,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";
@@ -5095,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;
};
}>;
}>;
@@ -5688,11 +5665,7 @@ export const UserSocialsFieldsFragmentDoc = gql`
id
username
profileImageUri
followers {
id
username
profileImageUri
}
isFollowedByCurrentUser
}
`;
export const VideoCardFieldsFragmentDoc = gql`
@@ -6569,6 +6542,7 @@ export const GetVideoFeedDocument = gql`
hasNextPage
endCursor
}
hasFollowing
}
}
${VideoCardFieldsFragmentDoc}
@@ -8996,17 +8970,9 @@ export type GetUserTagsQueryResult = Apollo.QueryResult<
export const FollowUserDocument = gql`
mutation followUser($followedUserId: Int!) {
followUser(followedUserId: $followedUserId) {
username
id
following {
id
username
}
followers {
id
username
}
}
}
`;
export type FollowUserMutationFn = Apollo.MutationFunction<
@@ -9055,17 +9021,9 @@ export type FollowUserMutationOptions = Apollo.BaseMutationOptions<
export const UnfollowUserDocument = gql`
mutation unfollowUser($followedUserId: Int!) {
unfollowUser(followedUserId: $followedUserId) {
username
id
following {
id
username
}
followers {
id
username
}
}
}
`;
export type UnfollowUserMutationFn = Apollo.MutationFunction<
@@ -9119,11 +9077,13 @@ export const GetUserFollowingFollowersDocument = gql`
id
username
profileImageUri
isFollowedByCurrentUser
}
followers {
id
username
profileImageUri
isFollowedByCurrentUser
}
}
}

View File

@@ -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
}
}

View File

@@ -88,32 +88,16 @@ query GetUserTags {
mutation followUser($followedUserId: Int!) {
followUser(followedUserId: $followedUserId) {
username
id
following {
id
username
}
followers {
id
username
}
}
}
mutation unfollowUser($followedUserId: Int!) {
unfollowUser(followedUserId: $followedUserId) {
username
id
following {
id
username
}
followers {
id
username
}
}
}
query getUserFollowingFollowers {
@@ -123,11 +107,13 @@ query getUserFollowingFollowers {
id
username
profileImageUri
isFollowedByCurrentUser
}
followers {
id
username
profileImageUri
isFollowedByCurrentUser
}
}
}

View File

@@ -86,11 +86,7 @@ fragment UserSocialsFields on UserGQL {
id
username
profileImageUri
followers {
id
username
profileImageUri
}
isFollowedByCurrentUser
}
query GetVideoSocialDetailsById($videoId: Int!) {

View File

@@ -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
@@ -368,6 +374,7 @@ type UserGQL {
agreesToMarketing: Boolean
following: [UserGQL!]
followers: [UserGQL!]
isFollowedByCurrentUser: Boolean
}
type ShotGQL {
@@ -647,6 +654,7 @@ input VideoFeedInputGQL @oneOf {
followedByUserId: Int
userId: Int
allUsers: Boolean
home: Boolean
}
type MakePercentageIntervalGQL {
@@ -715,6 +723,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 +955,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!