Compare commits

..

11 Commits

Author SHA1 Message Date
Dean Wenstrand
6d5cd9b1ed Add lean GetLastSessionDate + GetShotClipRanges queries
All checks were successful
Tests / Tests (pull_request) Successful in 24s
- GetLastSessionDate: most-recent session startTime only, for the Home
  recency nudge (was pulling the full VideoCardFields payload).
- GetShotClipRanges / ShotClipRange: per-shot frame/time window only, for
  the inline session player's condensed playback (was pulling
  ShotWithAllFeatures — features, serialized paths, annotations — x500).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:27:42 -07:00
Dean Wenstrand
d59e21c10e Add GetVideoCard query (single-video VideoCardFields)
Returns the full VideoCardFields fragment for one video id, so the
session-detail meta header can share one source of truth (and the
normalized Apollo cache) with the feed card instead of stitching
together GetVideoDetails + GetVideoSocialDetailsById.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 14:04:33 -07:00
Dean Wenstrand
9250e4c639 Add username + profileImageUri to PlayerClusterGQL
All checks were successful
Tests / Tests (pull_request) Successful in 9s
The labeling UI was falling back to "user N" for any assigned cluster
whose user wasn't the video owner — e.g. an admin re-labels a video
they don't own. With the resolver now resolving usernames for every
confirmed cluster, the FE can render real names regardless of who's
viewing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:25:34 -07:00
Dean Wenstrand
5cf2dbaf01 Add averageDifficulty to PlayerSummaryFields
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Regenerated via `just gql` after BE added average_difficulty to
PlayerSummaryGQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:34:25 -07:00
239a143554 Merge pull request 'Regenerate schema + add longestRun to PlayerSummaryFields' (#244) from dean/player-summaries-longest-run-types into master
Reviewed-on: #244
2026-05-11 20:29:39 +00:00
Dean Wenstrand
296522afb8 Regenerate schema + add longestRun to PlayerSummaryFields
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Generated by `just gql` after BE added longest_run to PlayerSummaryGQL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:21:47 -07:00
f42579076e Merge pull request 'Add score to PlayerSummaryFields + PlayerClusterFields fragments' (#243) from dean/video-match-score-types into master
Reviewed-on: #243
2026-05-11 17:43:43 +00:00
Dean Wenstrand
0c9eb4945a Add score to PlayerSummaryFields + PlayerClusterFields fragments
All checks were successful
Tests / Tests (pull_request) Successful in 9s
Picks up the BE additions in railbird PR dean/video-match-score:
new `score` field on PlayerClusterGQL and PlayerSummaryGQL, and
a new `score` input field on ClusterAssignmentInput.

Generated by `just gql`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 10:22:58 -07:00
1182c15004 Merge pull request 'Add playerSummaries to schema + Video fragments' (#242) from dean/player-summaries-types into master
Reviewed-on: #242
2026-05-09 19:34:40 +00:00
Dean Wenstrand
755336b16a Add playerSummaries to schema + Video fragments
All checks were successful
Tests / Tests (pull_request) Successful in 10s
Backs the multi-player vs UI: feed cards and the detail page both
read `video.playerSummaries`, a per-cluster rollup with username,
profile image, representative full-frame URL, makes/total/percentage.

  - PlayerSummaryFields fragment in shooter.gql
  - VideoCardFields (feed) and GetVideoDetails (detail) include
    playerSummaries via the new fragment
  - VideoCardFields tag selection extended to include tagClasses,
    needed for the FE's player_count detection

Generated by `just gql` from the BE additions in railbird PR
dean/video-player-summaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:02:57 -07:00
c1efe9f5f2 Merge pull request 'Add VideoPlayerClusters query + FinalizePlayerAssignments mutation' (#241) from dean/player-labeling-ops into master
Reviewed-on: #241
2026-05-09 04:46:10 +00:00
6 changed files with 665 additions and 3 deletions

View File

@@ -207,6 +207,7 @@ export enum ClientUploadStatusEnum {
export type ClusterAssignmentInput = { export type ClusterAssignmentInput = {
clusterId: Scalars["Int"]["input"]; clusterId: Scalars["Int"]["input"];
score?: InputMaybe<Scalars["Int"]["input"]>;
userId?: InputMaybe<Scalars["Int"]["input"]>; userId?: InputMaybe<Scalars["Int"]["input"]>;
}; };
@@ -2700,8 +2701,11 @@ export type PlayerClusterGql = {
clusterId: Scalars["Int"]["output"]; clusterId: Scalars["Int"]["output"];
confirmed: Scalars["Boolean"]["output"]; confirmed: Scalars["Boolean"]["output"];
nShots: Scalars["Int"]["output"]; nShots: Scalars["Int"]["output"];
profileImageUri?: Maybe<Scalars["String"]["output"]>;
score?: Maybe<Scalars["Int"]["output"]>;
shots: Array<PlayerClusterShotGql>; shots: Array<PlayerClusterShotGql>;
userId?: Maybe<Scalars["Int"]["output"]>; userId?: Maybe<Scalars["Int"]["output"]>;
username?: Maybe<Scalars["String"]["output"]>;
videoId: Scalars["Int"]["output"]; videoId: Scalars["Int"]["output"];
}; };
@@ -2718,6 +2722,22 @@ export type PlayerClusterShotGql = {
shotId: Scalars["Int"]["output"]; shotId: Scalars["Int"]["output"];
}; };
export type PlayerSummaryGql = {
__typename?: "PlayerSummaryGQL";
averageDifficulty?: Maybe<Scalars["Float"]["output"]>;
averageTimeBetweenShots?: Maybe<Scalars["Float"]["output"]>;
clusterId: Scalars["Int"]["output"];
longestRun: Scalars["Int"]["output"];
makePercentage: Scalars["Float"]["output"];
profileImageUri?: Maybe<Scalars["String"]["output"]>;
representativeFullFrameUrl?: Maybe<Scalars["String"]["output"]>;
score?: Maybe<Scalars["Int"]["output"]>;
totalShots: Scalars["Int"]["output"];
totalShotsMade: Scalars["Int"]["output"];
userId?: Maybe<Scalars["Int"]["output"]>;
username?: Maybe<Scalars["String"]["output"]>;
};
export enum PocketEnum { export enum PocketEnum {
Corner = "CORNER", Corner = "CORNER",
Side = "SIDE", Side = "SIDE",
@@ -3557,6 +3577,7 @@ export type VideoGql = {
medianRun?: Maybe<Scalars["Float"]["output"]>; medianRun?: Maybe<Scalars["Float"]["output"]>;
name?: Maybe<Scalars["String"]["output"]>; name?: Maybe<Scalars["String"]["output"]>;
owner?: Maybe<UserGql>; owner?: Maybe<UserGql>;
playerSummaries: Array<PlayerSummaryGql>;
playlist?: Maybe<HlsPlaylistGql>; playlist?: Maybe<HlsPlaylistGql>;
pocketSize?: Maybe<Scalars["Float"]["output"]>; pocketSize?: Maybe<Scalars["Float"]["output"]>;
private: Scalars["Boolean"]["output"]; private: Scalars["Boolean"]["output"];
@@ -4174,7 +4195,26 @@ export type GetFeedQuery = {
lastIntendedSegmentBound?: number | null; lastIntendedSegmentBound?: number | null;
streamSegmentType: StreamSegmentTypeEnum; streamSegmentType: StreamSegmentTypeEnum;
} | null; } | null;
tags: Array<{ __typename?: "VideoTag"; name: string }>; tags: Array<{
__typename?: "VideoTag";
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
}>;
currentProcessing?: { currentProcessing?: {
__typename?: "VideoProcessingGQL"; __typename?: "VideoProcessingGQL";
id: number; id: number;
@@ -4259,7 +4299,26 @@ export type VideoCardFieldsFragment = {
lastIntendedSegmentBound?: number | null; lastIntendedSegmentBound?: number | null;
streamSegmentType: StreamSegmentTypeEnum; streamSegmentType: StreamSegmentTypeEnum;
} | null; } | null;
tags: Array<{ __typename?: "VideoTag"; name: string }>; tags: Array<{
__typename?: "VideoTag";
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
}>;
currentProcessing?: { currentProcessing?: {
__typename?: "VideoProcessingGQL"; __typename?: "VideoProcessingGQL";
id: number; id: number;
@@ -4318,6 +4377,24 @@ export type GetVideoFeedSessionCountQuery = {
}; };
}; };
export type GetLastSessionDateQueryVariables = Exact<{
filters?: InputMaybe<VideoFilterInput>;
includePrivate?: InputMaybe<IncludePrivateEnum>;
feedInput?: InputMaybe<VideoFeedInputGql>;
}>;
export type GetLastSessionDateQuery = {
__typename?: "Query";
getFeedVideos: {
__typename?: "VideoHistoryGQL";
videos: Array<{
__typename?: "VideoGQL";
id: number;
startTime?: any | null;
}>;
};
};
export type GetVideoFeedQueryVariables = Exact<{ export type GetVideoFeedQueryVariables = Exact<{
limit?: Scalars["Int"]["input"]; limit?: Scalars["Int"]["input"];
after?: InputMaybe<Scalars["String"]["input"]>; after?: InputMaybe<Scalars["String"]["input"]>;
@@ -4358,7 +4435,26 @@ export type GetVideoFeedQuery = {
lastIntendedSegmentBound?: number | null; lastIntendedSegmentBound?: number | null;
streamSegmentType: StreamSegmentTypeEnum; streamSegmentType: StreamSegmentTypeEnum;
} | null; } | null;
tags: Array<{ __typename?: "VideoTag"; name: string }>; tags: Array<{
__typename?: "VideoTag";
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
}>;
currentProcessing?: { currentProcessing?: {
__typename?: "VideoProcessingGQL"; __typename?: "VideoProcessingGQL";
id: number; id: number;
@@ -4929,6 +5025,22 @@ export type GetRunsWithTimestampsQuery = {
}; };
}; };
export type PlayerSummaryFieldsFragment = {
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
};
export type PlayerClusterShotFieldsFragment = { export type PlayerClusterShotFieldsFragment = {
__typename?: "PlayerClusterShotGQL"; __typename?: "PlayerClusterShotGQL";
shotId: number; shotId: number;
@@ -4948,7 +5060,10 @@ export type PlayerClusterFieldsFragment = {
clusterId: number; clusterId: number;
nShots: number; nShots: number;
userId?: number | null; userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
confirmed: boolean; confirmed: boolean;
score?: number | null;
shots: Array<{ shots: Array<{
__typename?: "PlayerClusterShotGQL"; __typename?: "PlayerClusterShotGQL";
shotId: number; shotId: number;
@@ -4975,7 +5090,10 @@ export type VideoPlayerClustersQuery = {
clusterId: number; clusterId: number;
nShots: number; nShots: number;
userId?: number | null; userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
confirmed: boolean; confirmed: boolean;
score?: number | null;
shots: Array<{ shots: Array<{
__typename?: "PlayerClusterShotGQL"; __typename?: "PlayerClusterShotGQL";
shotId: number; shotId: number;
@@ -5003,7 +5121,10 @@ export type FinalizePlayerAssignmentsMutation = {
clusterId: number; clusterId: number;
nShots: number; nShots: number;
userId?: number | null; userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
confirmed: boolean; confirmed: boolean;
score?: number | null;
shots: Array<{ shots: Array<{
__typename?: "PlayerClusterShotGQL"; __typename?: "PlayerClusterShotGQL";
shotId: number; shotId: number;
@@ -5359,6 +5480,39 @@ export type GetShotsByIdsQuery = {
}>; }>;
}; };
export type ShotClipRangeFragment = {
__typename?: "ShotGQL";
id: number;
videoId: number;
startFrame: number;
endFrame: number;
startTime: number;
endTime: number;
};
export type GetShotClipRangesQueryVariables = Exact<{
filterInput: FilterInput;
shotsOrdering?: InputMaybe<GetShotsOrdering>;
limit?: InputMaybe<Scalars["Int"]["input"]>;
}>;
export type GetShotClipRangesQuery = {
__typename?: "Query";
getOrderedShots: {
__typename?: "GetShotsResult";
count?: number | null;
shots: Array<{
__typename?: "ShotGQL";
id: number;
videoId: number;
startFrame: number;
endFrame: number;
startTime: number;
endTime: number;
}>;
};
};
export type ShotWithAllFeaturesFragment = { export type ShotWithAllFeaturesFragment = {
__typename?: "ShotGQL"; __typename?: "ShotGQL";
id: number; id: number;
@@ -5955,6 +6109,21 @@ export type GetVideoDetailsQuery = {
name: string; name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>; tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>; }>;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
}>;
}; };
}; };
@@ -6022,6 +6191,102 @@ export type GetVideoSocialDetailsByIdQuery = {
}; };
}; };
export type GetVideoCardQueryVariables = Exact<{
videoId: Scalars["Int"]["input"];
}>;
export type GetVideoCardQuery = {
__typename?: "Query";
getVideo: {
__typename?: "VideoGQL";
id: number;
name?: string | null;
screenshotUri?: string | null;
totalShots: number;
makePercentage: number;
averageTimeBetweenShots?: number | null;
averageDifficulty?: number | null;
startTime?: any | null;
private: boolean;
elapsedTime?: number | null;
tableSize: number;
pocketSize?: number | null;
owner?: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
} | null;
stream?: {
__typename?: "UploadStreamGQL";
id: string;
lastIntendedSegmentBound?: number | null;
streamSegmentType: StreamSegmentTypeEnum;
} | null;
tags: Array<{
__typename?: "VideoTag";
name: string;
tagClasses: Array<{ __typename?: "VideoTagClass"; name: string }>;
}>;
playerSummaries: Array<{
__typename?: "PlayerSummaryGQL";
clusterId: number;
userId?: number | null;
username?: string | null;
profileImageUri?: string | null;
representativeFullFrameUrl?: string | null;
totalShots: number;
totalShotsMade: number;
makePercentage: number;
score?: number | null;
longestRun: number;
averageDifficulty?: number | null;
averageTimeBetweenShots?: number | null;
}>;
currentProcessing?: {
__typename?: "VideoProcessingGQL";
id: number;
status: ProcessingStatusEnum;
} | null;
reactions: Array<{
__typename?: "ReactionGQL";
videoId: number;
reaction: ReactionEnum;
user: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
isFollowedByCurrentUser?: boolean | null;
};
}>;
comments: Array<{
__typename?: "CommentGQL";
id: number;
message: string;
user: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
isFollowedByCurrentUser?: boolean | null;
};
replies: Array<{
__typename?: "CommentGQL";
id: number;
message: string;
user: {
__typename?: "UserGQL";
id: number;
username: string;
profileImageUri?: string | null;
isFollowedByCurrentUser?: boolean | null;
};
}>;
}>;
};
};
export type GetVideosQueryVariables = Exact<{ export type GetVideosQueryVariables = Exact<{
videoIds: Array<Scalars["Int"]["input"]> | Scalars["Int"]["input"]; videoIds: Array<Scalars["Int"]["input"]> | Scalars["Int"]["input"];
}>; }>;
@@ -6612,6 +6877,22 @@ export type GetUploadStreamsWithDetailsQuery = {
}; };
}; };
export const PlayerSummaryFieldsFragmentDoc = gql`
fragment PlayerSummaryFields on PlayerSummaryGQL {
clusterId
userId
username
profileImageUri
representativeFullFrameUrl
totalShots
totalShotsMade
makePercentage
score
longestRun
averageDifficulty
averageTimeBetweenShots
}
`;
export const UserSocialsFieldsFragmentDoc = gql` export const UserSocialsFieldsFragmentDoc = gql`
fragment UserSocialsFields on UserGQL { fragment UserSocialsFields on UserGQL {
id id
@@ -6646,6 +6927,12 @@ export const VideoCardFieldsFragmentDoc = gql`
pocketSize pocketSize
tags { tags {
name name
tagClasses {
name
}
}
playerSummaries {
...PlayerSummaryFields
} }
currentProcessing { currentProcessing {
id id
@@ -6673,6 +6960,7 @@ export const VideoCardFieldsFragmentDoc = gql`
} }
} }
} }
${PlayerSummaryFieldsFragmentDoc}
${UserSocialsFieldsFragmentDoc} ${UserSocialsFieldsFragmentDoc}
`; `;
export const MedalFieldsFragmentDoc = gql` export const MedalFieldsFragmentDoc = gql`
@@ -6741,13 +7029,26 @@ export const PlayerClusterFieldsFragmentDoc = gql`
clusterId clusterId
nShots nShots
userId userId
username
profileImageUri
confirmed confirmed
score
shots { shots {
...PlayerClusterShotFields ...PlayerClusterShotFields
} }
} }
${PlayerClusterShotFieldsFragmentDoc} ${PlayerClusterShotFieldsFragmentDoc}
`; `;
export const ShotClipRangeFragmentDoc = gql`
fragment ShotClipRange on ShotGQL {
id
videoId
startFrame
endFrame
startTime @client
endTime @client
}
`;
export const ShotWithAllFeaturesFragmentDoc = gql` export const ShotWithAllFeaturesFragmentDoc = gql`
fragment ShotWithAllFeatures on ShotGQL { fragment ShotWithAllFeatures on ShotGQL {
id id
@@ -8832,6 +9133,93 @@ export type GetVideoFeedSessionCountQueryResult = Apollo.QueryResult<
GetVideoFeedSessionCountQuery, GetVideoFeedSessionCountQuery,
GetVideoFeedSessionCountQueryVariables GetVideoFeedSessionCountQueryVariables
>; >;
export const GetLastSessionDateDocument = gql`
query GetLastSessionDate(
$filters: VideoFilterInput = null
$includePrivate: IncludePrivateEnum = MINE
$feedInput: VideoFeedInputGQL = null
) {
getFeedVideos(
limit: 1
filters: $filters
includePrivate: $includePrivate
feedInput: $feedInput
) {
videos {
id
startTime
}
}
}
`;
/**
* __useGetLastSessionDateQuery__
*
* To run a query within a React component, call `useGetLastSessionDateQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLastSessionDateQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetLastSessionDateQuery({
* variables: {
* filters: // value for 'filters'
* includePrivate: // value for 'includePrivate'
* feedInput: // value for 'feedInput'
* },
* });
*/
export function useGetLastSessionDateQuery(
baseOptions?: Apollo.QueryHookOptions<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>(GetLastSessionDateDocument, options);
}
export function useGetLastSessionDateLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>(GetLastSessionDateDocument, options);
}
export function useGetLastSessionDateSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>(GetLastSessionDateDocument, options);
}
export type GetLastSessionDateQueryHookResult = ReturnType<
typeof useGetLastSessionDateQuery
>;
export type GetLastSessionDateLazyQueryHookResult = ReturnType<
typeof useGetLastSessionDateLazyQuery
>;
export type GetLastSessionDateSuspenseQueryHookResult = ReturnType<
typeof useGetLastSessionDateSuspenseQuery
>;
export type GetLastSessionDateQueryResult = Apollo.QueryResult<
GetLastSessionDateQuery,
GetLastSessionDateQueryVariables
>;
export const GetVideoFeedDocument = gql` export const GetVideoFeedDocument = gql`
query GetVideoFeed( query GetVideoFeed(
$limit: Int! = 5 $limit: Int! = 5
@@ -11135,6 +11523,93 @@ export type GetShotsByIdsQueryResult = Apollo.QueryResult<
GetShotsByIdsQuery, GetShotsByIdsQuery,
GetShotsByIdsQueryVariables GetShotsByIdsQueryVariables
>; >;
export const GetShotClipRangesDocument = gql`
query GetShotClipRanges(
$filterInput: FilterInput!
$shotsOrdering: GetShotsOrdering
$limit: Int
) {
getOrderedShots(
filterInput: $filterInput
shotsOrdering: $shotsOrdering
limit: $limit
) {
count
shots {
...ShotClipRange
}
}
}
${ShotClipRangeFragmentDoc}
`;
/**
* __useGetShotClipRangesQuery__
*
* To run a query within a React component, call `useGetShotClipRangesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetShotClipRangesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetShotClipRangesQuery({
* variables: {
* filterInput: // value for 'filterInput'
* shotsOrdering: // value for 'shotsOrdering'
* limit: // value for 'limit'
* },
* });
*/
export function useGetShotClipRangesQuery(
baseOptions: Apollo.QueryHookOptions<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>(GetShotClipRangesDocument, options);
}
export function useGetShotClipRangesLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>(GetShotClipRangesDocument, options);
}
export function useGetShotClipRangesSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>(GetShotClipRangesDocument, options);
}
export type GetShotClipRangesQueryHookResult = ReturnType<
typeof useGetShotClipRangesQuery
>;
export type GetShotClipRangesLazyQueryHookResult = ReturnType<
typeof useGetShotClipRangesLazyQuery
>;
export type GetShotClipRangesSuspenseQueryHookResult = ReturnType<
typeof useGetShotClipRangesSuspenseQuery
>;
export type GetShotClipRangesQueryResult = Apollo.QueryResult<
GetShotClipRangesQuery,
GetShotClipRangesQueryVariables
>;
export const EditShotDocument = gql` export const EditShotDocument = gql`
mutation EditShot($shotId: Int!, $fieldsToEdit: EditableShotFieldInputGQL!) { mutation EditShot($shotId: Int!, $fieldsToEdit: EditableShotFieldInputGQL!) {
editShot(shotId: $shotId, fieldsToEdit: $fieldsToEdit) { editShot(shotId: $shotId, fieldsToEdit: $fieldsToEdit) {
@@ -12666,8 +13141,12 @@ export const GetVideoDetailsDocument = gql`
} }
name name
} }
playerSummaries {
...PlayerSummaryFields
}
} }
} }
${PlayerSummaryFieldsFragmentDoc}
`; `;
/** /**
@@ -12846,6 +13325,80 @@ export type GetVideoSocialDetailsByIdQueryResult = Apollo.QueryResult<
GetVideoSocialDetailsByIdQuery, GetVideoSocialDetailsByIdQuery,
GetVideoSocialDetailsByIdQueryVariables GetVideoSocialDetailsByIdQueryVariables
>; >;
export const GetVideoCardDocument = gql`
query GetVideoCard($videoId: Int!) {
getVideo(videoId: $videoId) {
...VideoCardFields
}
}
${VideoCardFieldsFragmentDoc}
`;
/**
* __useGetVideoCardQuery__
*
* To run a query within a React component, call `useGetVideoCardQuery` and pass it any options that fit your needs.
* When your component renders, `useGetVideoCardQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetVideoCardQuery({
* variables: {
* videoId: // value for 'videoId'
* },
* });
*/
export function useGetVideoCardQuery(
baseOptions: Apollo.QueryHookOptions<
GetVideoCardQuery,
GetVideoCardQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<GetVideoCardQuery, GetVideoCardQueryVariables>(
GetVideoCardDocument,
options,
);
}
export function useGetVideoCardLazyQuery(
baseOptions?: Apollo.LazyQueryHookOptions<
GetVideoCardQuery,
GetVideoCardQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<GetVideoCardQuery, GetVideoCardQueryVariables>(
GetVideoCardDocument,
options,
);
}
export function useGetVideoCardSuspenseQuery(
baseOptions?: Apollo.SuspenseQueryHookOptions<
GetVideoCardQuery,
GetVideoCardQueryVariables
>,
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useSuspenseQuery<GetVideoCardQuery, GetVideoCardQueryVariables>(
GetVideoCardDocument,
options,
);
}
export type GetVideoCardQueryHookResult = ReturnType<
typeof useGetVideoCardQuery
>;
export type GetVideoCardLazyQueryHookResult = ReturnType<
typeof useGetVideoCardLazyQuery
>;
export type GetVideoCardSuspenseQueryHookResult = ReturnType<
typeof useGetVideoCardSuspenseQuery
>;
export type GetVideoCardQueryResult = Apollo.QueryResult<
GetVideoCardQuery,
GetVideoCardQueryVariables
>;
export const GetVideosDocument = gql` export const GetVideosDocument = gql`
query GetVideos($videoIds: [Int!]!) { query GetVideos($videoIds: [Int!]!) {
getVideos(videoIds: $videoIds) { getVideos(videoIds: $videoIds) {

View File

@@ -47,6 +47,12 @@ fragment VideoCardFields on VideoGQL {
pocketSize pocketSize
tags { tags {
name name
tagClasses {
name
}
}
playerSummaries {
...PlayerSummaryFields
} }
currentProcessing { currentProcessing {
id id
@@ -95,6 +101,27 @@ query GetVideoFeedSessionCount(
} }
} }
# Minimal query for the Home recency nudge ("you haven't recorded in N days").
# Only the most recent session's start time — avoids pulling the full
# VideoCardFields payload (reactions, comments, player summaries, etc.).
query GetLastSessionDate(
$filters: VideoFilterInput = null
$includePrivate: IncludePrivateEnum = MINE
$feedInput: VideoFeedInputGQL = null
) {
getFeedVideos(
limit: 1
filters: $filters
includePrivate: $includePrivate
feedInput: $feedInput
) {
videos {
id
startTime
}
}
}
query GetVideoFeed( query GetVideoFeed(
$limit: Int! = 5 $limit: Int! = 5
$after: String = null $after: String = null

View File

@@ -1,3 +1,18 @@
fragment PlayerSummaryFields on PlayerSummaryGQL {
clusterId
userId
username
profileImageUri
representativeFullFrameUrl
totalShots
totalShotsMade
makePercentage
score
longestRun
averageDifficulty
averageTimeBetweenShots
}
fragment PlayerClusterShotFields on PlayerClusterShotGQL { fragment PlayerClusterShotFields on PlayerClusterShotGQL {
shotId shotId
bboxX1 bboxX1
@@ -15,7 +30,10 @@ fragment PlayerClusterFields on PlayerClusterGQL {
clusterId clusterId
nShots nShots
userId userId
username
profileImageUri
confirmed confirmed
score
shots { shots {
...PlayerClusterShotFields ...PlayerClusterShotFields
} }

View File

@@ -132,6 +132,38 @@ query GetShotsByIds($ids: [Int!]!) {
} }
} }
# Lightweight clip boundaries for condensed session playback. The inline
# session player only needs each shot's frame/time window to seek between
# shots — this skips the heavy ShotWithAllFeatures payload (cue/pocketing
# features, serialized shot paths, annotations, nested video/playlist). The
# startTime/endTime @client resolvers derive their values from the frame
# fields + the video (looked up internally), so this is all they require.
fragment ShotClipRange on ShotGQL {
id
videoId
startFrame
endFrame
startTime @client
endTime @client
}
query GetShotClipRanges(
$filterInput: FilterInput!
$shotsOrdering: GetShotsOrdering
$limit: Int
) {
getOrderedShots(
filterInput: $filterInput
shotsOrdering: $shotsOrdering
limit: $limit
) {
count
shots {
...ShotClipRange
}
}
}
fragment ShotWithAllFeatures on ShotGQL { fragment ShotWithAllFeatures on ShotGQL {
id id
videoId videoId

View File

@@ -83,6 +83,9 @@ query GetVideoDetails($videoId: Int!) {
} }
name name
} }
playerSummaries {
...PlayerSummaryFields
}
} }
} }
@@ -136,6 +139,15 @@ query GetVideoSocialDetailsById($videoId: Int!) {
} }
} }
# Full card payload for a single video — reuses the same VideoCardFields
# fragment the feed list uses, so the session-detail meta header shares one
# source of truth (and the normalized Apollo cache) with the feed card.
query GetVideoCard($videoId: Int!) {
getVideo(videoId: $videoId) {
...VideoCardFields
}
}
query GetVideos($videoIds: [Int!]!) { query GetVideos($videoIds: [Int!]!) {
getVideos(videoIds: $videoIds) { getVideos(videoIds: $videoIds) {
...VideoStreamMetadata ...VideoStreamMetadata

View File

@@ -406,6 +406,7 @@ type VideoGQL {
currentProcessing: VideoProcessingGQL currentProcessing: VideoProcessingGQL
reactions: [ReactionGQL!]! reactions: [ReactionGQL!]!
comments: [CommentGQL!]! comments: [CommentGQL!]!
playerSummaries: [PlayerSummaryGQL!]!
} }
type ShotGQL { type ShotGQL {
@@ -665,6 +666,21 @@ type CommentGQL {
replies: [CommentGQL!]! replies: [CommentGQL!]!
} }
type PlayerSummaryGQL {
clusterId: Int!
userId: Int
username: String
profileImageUri: String
representativeFullFrameUrl: String
totalShots: Int!
totalShotsMade: Int!
makePercentage: Float!
score: Int
longestRun: Int!
averageDifficulty: Float
averageTimeBetweenShots: Float
}
type DeployedConfigGQL { type DeployedConfigGQL {
allowNewUsers: Boolean! allowNewUsers: Boolean!
firebase: Boolean! firebase: Boolean!
@@ -866,7 +882,10 @@ type PlayerClusterGQL {
clusterId: Int! clusterId: Int!
nShots: Int! nShots: Int!
userId: Int userId: Int
username: String
profileImageUri: String
confirmed: Boolean! confirmed: Boolean!
score: Int
shots: [PlayerClusterShotGQL!]! shots: [PlayerClusterShotGQL!]!
} }
@@ -1199,6 +1218,7 @@ input FinalizePlayerAssignmentsInput {
input ClusterAssignmentInput { input ClusterAssignmentInput {
clusterId: Int! clusterId: Int!
userId: Int = null userId: Int = null
score: Int = null
} }
input ShotMoveInput { input ShotMoveInput {