tfrere HF Staff Cursor commited on
Commit
0fe543d
·
1 Parent(s): 9ce28e1

feat: restore interactive like with session cookies

Browse files

Since the site is served from huggingface.co/reachy-mini (same origin
as the API), we can use credentials:'include' to send HF session
cookies with like requests. This bypasses the OAuth token limitation
on the POST /api/spaces/{id}/like endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>

src/components/InstallModal.jsx CHANGED
@@ -21,7 +21,7 @@ import ComputerIcon from '@mui/icons-material/Computer';
21
  import { useAuth } from '../context/AuthContext';
22
 
23
  function InstallModal({ open, onClose, app }) {
24
- const { isLoggedIn, isSpaceLiked, openSpaceForLike, login } = useAuth();
25
  // Detect Linux users
26
  const isLinux = useMemo(() => {
27
  if (typeof navigator === 'undefined') return false;
@@ -171,19 +171,14 @@ function InstallModal({ open, onClose, app }) {
171
  {/* Stats row */}
172
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
173
  <Tooltip
174
- title={isLoggedIn ? 'Like on Hugging Face' : 'Sign in to like this app'}
175
  arrow
176
  placement="top"
 
177
  >
178
  <Box
179
  component="button"
180
- onClick={() => {
181
- if (!isLoggedIn) {
182
- login();
183
- return;
184
- }
185
- openSpaceForLike(app.id);
186
- }}
187
  sx={{
188
  display: 'flex',
189
  alignItems: 'center',
 
21
  import { useAuth } from '../context/AuthContext';
22
 
23
  function InstallModal({ open, onClose, app }) {
24
+ const { isLoggedIn, isSpaceLiked, toggleLike } = useAuth();
25
  // Detect Linux users
26
  const isLinux = useMemo(() => {
27
  if (typeof navigator === 'undefined') return false;
 
171
  {/* Stats row */}
172
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
173
  <Tooltip
174
+ title={isLoggedIn ? '' : 'Sign in to like this app'}
175
  arrow
176
  placement="top"
177
+ disableHoverListener={isLoggedIn}
178
  >
179
  <Box
180
  component="button"
181
+ onClick={() => toggleLike(app.id)}
 
 
 
 
 
 
182
  sx={{
183
  display: 'flex',
184
  alignItems: 'center',
src/context/AuthContext.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useState, useEffect, useCallback } from 'react';
2
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
3
 
4
  const AuthContext = createContext(null);
@@ -47,6 +47,7 @@ export function AuthProvider({ children }) {
47
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
48
  const [isLoading, setIsLoading] = useState(true);
49
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
 
50
 
51
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
52
  useEffect(() => {
@@ -123,30 +124,66 @@ export function AuthProvider({ children }) {
123
  );
124
 
125
  /**
126
- * Open the HF Space page so the user can like it natively.
127
- * HuggingFace intentionally blocks programmatic likes via API tokens
128
- * to prevent spam ("To prevent spam usage, it is not possible to like a
129
- * repository from a script." — huggingface_hub SDK).
130
- * Reading likes works fine; writing requires the native HF session.
131
  */
132
- const openSpaceForLike = useCallback(
133
- (spaceId) => {
134
- if (!spaceId) return;
135
- window.open(`${HF_API}/spaces/${spaceId}`, '_blank', 'noopener,noreferrer');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  },
137
- []
138
  );
139
 
140
- /**
141
- * Re-fetch user's liked spaces (call after user returns from HF page).
142
- */
143
- const refreshLikes = useCallback(async () => {
144
- if (!user?.preferredUsername) return;
145
- const likes = await fetchUserLikedSpaces(user.preferredUsername);
146
- setLikedSpaceIds(likes);
147
- console.log(`[Auth] Refreshed likes: ${likes.size} liked spaces`);
148
- }, [user]);
149
-
150
  return (
151
  <AuthContext.Provider
152
  value={{
@@ -158,8 +195,7 @@ export function AuthProvider({ children }) {
158
  login,
159
  logout,
160
  isSpaceLiked,
161
- openSpaceForLike,
162
- refreshLikes,
163
  }}
164
  >
165
  {children}
 
1
+ import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
2
  import { oauthLoginUrl, oauthHandleRedirectIfPresent } from '@huggingface/hub';
3
 
4
  const AuthContext = createContext(null);
 
47
  const [likedSpaceIds, setLikedSpaceIds] = useState(new Set());
48
  const [isLoading, setIsLoading] = useState(true);
49
  const [oauthConfig, setOauthConfig] = useState(null); // { clientId, scopes }
50
+ const pendingLikes = useRef(new Set()); // Track in-flight like/unlike requests
51
 
52
  // On mount: fetch OAuth config + check if user just completed OAuth redirect
53
  useEffect(() => {
 
124
  );
125
 
126
  /**
127
+ * Toggle like/unlike with optimistic update.
128
+ * Uses credentials: 'include' to send HF session cookies — this works when
129
+ * the page is served from huggingface.co (same-origin with the API).
 
 
130
  */
131
+ const toggleLike = useCallback(
132
+ async (spaceId) => {
133
+ if (!user) {
134
+ login();
135
+ return;
136
+ }
137
+
138
+ const spaceIdLower = spaceId?.toLowerCase();
139
+ if (!spaceIdLower) return;
140
+
141
+ // Prevent duplicate requests for the same space
142
+ if (pendingLikes.current.has(spaceIdLower)) return;
143
+ pendingLikes.current.add(spaceIdLower);
144
+
145
+ const wasLiked = likedSpaceIds.has(spaceIdLower);
146
+ const method = wasLiked ? 'DELETE' : 'POST';
147
+
148
+ // Optimistic update
149
+ setLikedSpaceIds((prev) => {
150
+ const next = new Set(prev);
151
+ if (wasLiked) {
152
+ next.delete(spaceIdLower);
153
+ } else {
154
+ next.add(spaceIdLower);
155
+ }
156
+ return next;
157
+ });
158
+
159
+ try {
160
+ const response = await fetch(`${HF_API}/api/spaces/${spaceId}/like`, {
161
+ method,
162
+ credentials: 'include',
163
+ });
164
+
165
+ if (!response.ok) {
166
+ throw new Error(`Like API returned ${response.status}`);
167
+ }
168
+ } catch (err) {
169
+ console.error(`[Auth] Failed to ${wasLiked ? 'unlike' : 'like'} ${spaceId}:`, err);
170
+ // Revert optimistic update on error
171
+ setLikedSpaceIds((prev) => {
172
+ const reverted = new Set(prev);
173
+ if (wasLiked) {
174
+ reverted.add(spaceIdLower);
175
+ } else {
176
+ reverted.delete(spaceIdLower);
177
+ }
178
+ return reverted;
179
+ });
180
+ } finally {
181
+ pendingLikes.current.delete(spaceIdLower);
182
+ }
183
  },
184
+ [user, likedSpaceIds, login]
185
  );
186
 
 
 
 
 
 
 
 
 
 
 
187
  return (
188
  <AuthContext.Provider
189
  value={{
 
195
  login,
196
  logout,
197
  isSpaceLiked,
198
+ toggleLike,
 
199
  }}
200
  >
201
  {children}
src/pages/Apps.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useCallback, useEffect } from 'react';
2
  import {
3
  Box,
4
  Container,
@@ -30,7 +30,7 @@ import { useAuth } from '../context/AuthContext';
30
  import InstallModal from '../components/InstallModal';
31
 
32
  // App Card Component
33
- function AppCard({ app, onInstallClick, isLiked, onLikeClick, isLoggedIn }) {
34
  const isOfficial = app.isOfficial;
35
  const isPythonApp = app.extra?.isPythonApp !== false; // Default to true for backwards compatibility
36
  const cardData = app.extra?.cardData || {};
@@ -175,18 +175,19 @@ function AppCard({ app, onInstallClick, isLiked, onLikeClick, isLoggedIn }) {
175
  )}
176
  </Box>
177
 
178
- {/* Likes - Click opens HF Space page to like natively */}
179
  <Tooltip
180
- title={isLoggedIn ? 'Like on Hugging Face' : 'Sign in to like this app'}
181
  arrow
182
  placement="top"
 
183
  >
184
  <Box
185
  component="button"
186
  onClick={(e) => {
187
  e.preventDefault();
188
  e.stopPropagation();
189
- onLikeClick?.(app.id);
190
  }}
191
  sx={{
192
  display: 'flex',
@@ -333,7 +334,7 @@ function AppCard({ app, onInstallClick, isLiked, onLikeClick, isLoggedIn }) {
333
  export default function Apps() {
334
  // Get apps from context (cached globally)
335
  const { apps, loading, error } = useApps();
336
- const { user, isLoggedIn, isOAuthAvailable, login, logout, isSpaceLiked, openSpaceForLike, refreshLikes } = useAuth();
337
  const [searchQuery, setSearchQuery] = useState('');
338
  const [officialOnly, setOfficialOnly] = useState(false);
339
 
@@ -351,30 +352,13 @@ export default function Apps() {
351
  setSelectedApp(null);
352
  };
353
 
354
- // Handle like click: if logged in, open HF page; if not, prompt login
355
- const handleLikeClick = useCallback(
356
  (spaceId) => {
357
- if (!isLoggedIn) {
358
- login();
359
- return;
360
- }
361
- openSpaceForLike(spaceId);
362
  },
363
- [isLoggedIn, login, openSpaceForLike]
364
  );
365
 
366
- // Refresh likes when the tab regains focus (user may have liked on HF)
367
- useEffect(() => {
368
- if (!isLoggedIn) return;
369
- const handleVisibilityChange = () => {
370
- if (document.visibilityState === 'visible') {
371
- refreshLikes();
372
- }
373
- };
374
- document.addEventListener('visibilitychange', handleVisibilityChange);
375
- return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
376
- }, [isLoggedIn, refreshLikes]);
377
-
378
  // Filter apps based on search and official toggle
379
  const filteredApps = useMemo(() => {
380
  let result = apps;
@@ -714,7 +698,7 @@ export default function Apps() {
714
  app={app}
715
  onInstallClick={handleInstallClick}
716
  isLiked={isSpaceLiked(app.id)}
717
- onLikeClick={handleLikeClick}
718
  isLoggedIn={isLoggedIn}
719
  />
720
  ))}
 
1
+ import { useState, useMemo, useCallback } from 'react';
2
  import {
3
  Box,
4
  Container,
 
30
  import InstallModal from '../components/InstallModal';
31
 
32
  // App Card Component
33
+ function AppCard({ app, onInstallClick, isLiked, onToggleLike, isLoggedIn }) {
34
  const isOfficial = app.isOfficial;
35
  const isPythonApp = app.extra?.isPythonApp !== false; // Default to true for backwards compatibility
36
  const cardData = app.extra?.cardData || {};
 
175
  )}
176
  </Box>
177
 
178
+ {/* Likes - Interactive */}
179
  <Tooltip
180
+ title={isLoggedIn ? '' : 'Sign in to like this app'}
181
  arrow
182
  placement="top"
183
+ disableHoverListener={isLoggedIn}
184
  >
185
  <Box
186
  component="button"
187
  onClick={(e) => {
188
  e.preventDefault();
189
  e.stopPropagation();
190
+ onToggleLike?.(app.id);
191
  }}
192
  sx={{
193
  display: 'flex',
 
334
  export default function Apps() {
335
  // Get apps from context (cached globally)
336
  const { apps, loading, error } = useApps();
337
+ const { user, isLoggedIn, isOAuthAvailable, login, logout, isSpaceLiked, toggleLike } = useAuth();
338
  const [searchQuery, setSearchQuery] = useState('');
339
  const [officialOnly, setOfficialOnly] = useState(false);
340
 
 
352
  setSelectedApp(null);
353
  };
354
 
355
+ const handleToggleLike = useCallback(
 
356
  (spaceId) => {
357
+ toggleLike(spaceId);
 
 
 
 
358
  },
359
+ [toggleLike]
360
  );
361
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  // Filter apps based on search and official toggle
363
  const filteredApps = useMemo(() => {
364
  let result = apps;
 
698
  app={app}
699
  onInstallClick={handleInstallClick}
700
  isLiked={isSpaceLiked(app.id)}
701
+ onToggleLike={handleToggleLike}
702
  isLoggedIn={isLoggedIn}
703
  />
704
  ))}