Follow Users on the instance
This commit is contained in:
parent
cc59701337
commit
fe05e2ffa4
4 changed files with 262 additions and 2 deletions
|
|
@ -123,6 +123,9 @@ public class SecurityConfig {
|
|||
.requestMatchers(HttpMethod.GET, "/api/users/browse").permitAll() // Browse all users
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/*/followers").permitAll() // User followers list
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/*/following").permitAll() // User following list
|
||||
.requestMatchers(HttpMethod.GET, "/api/users/*/follow-status").permitAll() // Follow status check
|
||||
.requestMatchers(HttpMethod.POST, "/api/users/*/follow").authenticated() // Follow user
|
||||
.requestMatchers(HttpMethod.DELETE, "/api/users/*/follow").authenticated() // Unfollow user
|
||||
|
||||
// All other requests require authentication
|
||||
.anyRequest().authenticated()
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import org.springframework.web.bind.annotation.*;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
|
@ -271,4 +273,159 @@ public class UserController {
|
|||
log.debug("Found {} following for user {}", actorDTOs.size(), username);
|
||||
return ResponseEntity.ok(actorDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow a user.
|
||||
*
|
||||
* @param username the username to follow
|
||||
* @param userDetails the authenticated user
|
||||
* @return success response
|
||||
*/
|
||||
@PostMapping("/{username}/follow")
|
||||
public ResponseEntity<Map<String, Object>> followUser(
|
||||
@PathVariable String username,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
log.info("User {} attempting to follow {}", userDetails.getUsername(), username);
|
||||
|
||||
// Get the current user
|
||||
User currentUser = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Current user not found"));
|
||||
|
||||
// Get the user to follow
|
||||
User userToFollow = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
|
||||
// Cannot follow yourself
|
||||
if (currentUser.getId().equals(userToFollow.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Cannot follow yourself"));
|
||||
}
|
||||
|
||||
String followingActorUri = userToFollow.getActorUri(baseUrl);
|
||||
|
||||
// Check if already following
|
||||
Optional<Follow> existingFollow = followRepository.findByFollowerIdAndFollowingActorUri(
|
||||
currentUser.getId(), followingActorUri
|
||||
);
|
||||
|
||||
if (existingFollow.isPresent()) {
|
||||
if (existingFollow.get().getStatus() == Follow.FollowStatus.ACCEPTED) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Already following this user"));
|
||||
} else {
|
||||
// Update existing pending follow to accepted
|
||||
Follow follow = existingFollow.get();
|
||||
follow.setStatus(Follow.FollowStatus.ACCEPTED);
|
||||
followRepository.save(follow);
|
||||
log.info("Updated pending follow to accepted: {} -> {}", currentUser.getUsername(), username);
|
||||
}
|
||||
} else {
|
||||
// Create new follow relationship
|
||||
String activityId = baseUrl + "/activities/follow/" + UUID.randomUUID();
|
||||
Follow follow = Follow.builder()
|
||||
.followerId(currentUser.getId())
|
||||
.followingActorUri(followingActorUri)
|
||||
.status(Follow.FollowStatus.ACCEPTED) // Auto-accept for local-to-local follows
|
||||
.activityId(activityId)
|
||||
.build();
|
||||
followRepository.save(follow);
|
||||
log.info("Created new follow: {} -> {}", currentUser.getUsername(), username);
|
||||
}
|
||||
|
||||
// Get updated follower count
|
||||
long followersCount = followRepository.countAcceptedFollowersByActorUri(followingActorUri);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Successfully followed " + username,
|
||||
"followersCount", followersCount
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfollow a user.
|
||||
*
|
||||
* @param username the username to unfollow
|
||||
* @param userDetails the authenticated user
|
||||
* @return success response
|
||||
*/
|
||||
@DeleteMapping("/{username}/follow")
|
||||
public ResponseEntity<Map<String, Object>> unfollowUser(
|
||||
@PathVariable String username,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
log.info("User {} attempting to unfollow {}", userDetails.getUsername(), username);
|
||||
|
||||
// Get the current user
|
||||
User currentUser = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("Current user not found"));
|
||||
|
||||
// Get the user to unfollow
|
||||
User userToUnfollow = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
|
||||
String followingActorUri = userToUnfollow.getActorUri(baseUrl);
|
||||
|
||||
// Find the follow relationship
|
||||
Optional<Follow> follow = followRepository.findByFollowerIdAndFollowingActorUri(
|
||||
currentUser.getId(), followingActorUri
|
||||
);
|
||||
|
||||
if (follow.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Not following this user"));
|
||||
}
|
||||
|
||||
// Delete the follow relationship
|
||||
followRepository.delete(follow.get());
|
||||
log.info("Deleted follow: {} -> {}", currentUser.getUsername(), username);
|
||||
|
||||
// Get updated follower count
|
||||
long followersCount = followRepository.countAcceptedFollowersByActorUri(followingActorUri);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Successfully unfollowed " + username,
|
||||
"followersCount", followersCount
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is following a specific user.
|
||||
*
|
||||
* @param username the username to check
|
||||
* @param userDetails the authenticated user
|
||||
* @return follow status
|
||||
*/
|
||||
@GetMapping("/{username}/follow-status")
|
||||
public ResponseEntity<Map<String, Boolean>> getFollowStatus(
|
||||
@PathVariable String username,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
if (userDetails == null) {
|
||||
return ResponseEntity.ok(Map.of("isFollowing", false));
|
||||
}
|
||||
|
||||
User currentUser = userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElse(null);
|
||||
|
||||
if (currentUser == null) {
|
||||
return ResponseEntity.ok(Map.of("isFollowing", false));
|
||||
}
|
||||
|
||||
User targetUser = userRepository.findByUsername(username)
|
||||
.orElse(null);
|
||||
|
||||
if (targetUser == null) {
|
||||
return ResponseEntity.ok(Map.of("isFollowing", false));
|
||||
}
|
||||
|
||||
String followingActorUri = targetUser.getActorUri(baseUrl);
|
||||
Optional<Follow> follow = followRepository.findByFollowerIdAndFollowingActorUri(
|
||||
currentUser.getId(), followingActorUri
|
||||
);
|
||||
|
||||
boolean isFollowing = follow.isPresent() && follow.get().getStatus() == Follow.FollowStatus.ACCEPTED;
|
||||
|
||||
return ResponseEntity.ok(Map.of("isFollowing", isFollowing));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ public class UserDTO {
|
|||
private Long followersCount;
|
||||
private Long followingCount;
|
||||
|
||||
// Follow status (populated separately, indicates if the current user is following this user)
|
||||
private Boolean isFollowing;
|
||||
|
||||
/**
|
||||
* Creates a DTO from a User entity.
|
||||
* Note: email should only be included when user is viewing their own profile.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@
|
|||
</div>
|
||||
<div id="followButtonContainer" class="d-none">
|
||||
<button class="btn btn-primary" id="followBtn">
|
||||
<i class="bi bi-person-plus"></i> Follow
|
||||
<span id="followBtnIcon"><i class="bi bi-person-plus"></i></span>
|
||||
<span id="followBtnText">Follow</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -196,7 +197,103 @@
|
|||
document.getElementById('joinedDate').textContent = joinedDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
|
||||
// Show follow button if viewing another user's profile
|
||||
// TODO: implement follow functionality
|
||||
checkAndShowFollowButton();
|
||||
}
|
||||
|
||||
async function checkAndShowFollowButton() {
|
||||
// Check if user is authenticated
|
||||
if (!FitPubAuth.isAuthenticated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current user's username
|
||||
const currentUsername = FitPubAuth.getUsername();
|
||||
|
||||
// If viewing own profile, don't show follow button
|
||||
if (!currentUsername || currentUsername === targetUsername) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the follow button container
|
||||
document.getElementById('followButtonContainer').classList.remove('d-none');
|
||||
|
||||
// Add click event listener to follow button
|
||||
const followBtn = document.getElementById('followBtn');
|
||||
followBtn.addEventListener('click', handleFollowToggle);
|
||||
|
||||
// Check follow status
|
||||
await updateFollowButtonState();
|
||||
}
|
||||
|
||||
async function updateFollowButtonState() {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${targetUsername}/follow-status`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const isFollowing = data.isFollowing;
|
||||
|
||||
const followBtn = document.getElementById('followBtn');
|
||||
const followBtnIcon = document.getElementById('followBtnIcon');
|
||||
const followBtnText = document.getElementById('followBtnText');
|
||||
|
||||
if (isFollowing) {
|
||||
followBtn.className = 'btn btn-outline-danger';
|
||||
followBtnIcon.innerHTML = '<i class="bi bi-person-dash"></i>';
|
||||
followBtnText.textContent = 'Unfollow';
|
||||
followBtn.dataset.following = 'true';
|
||||
} else {
|
||||
followBtn.className = 'btn btn-primary';
|
||||
followBtnIcon.innerHTML = '<i class="bi bi-person-plus"></i>';
|
||||
followBtnText.textContent = 'Follow';
|
||||
followBtn.dataset.following = 'false';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking follow status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFollowToggle() {
|
||||
const followBtn = document.getElementById('followBtn');
|
||||
const isFollowing = followBtn.dataset.following === 'true';
|
||||
|
||||
// Disable button during request
|
||||
followBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const method = isFollowing ? 'DELETE' : 'POST';
|
||||
const response = await FitPubAuth.authenticatedFetch(`/api/users/${targetUsername}/follow`, {
|
||||
method: method
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
// Update follower count
|
||||
if (data.followersCount !== undefined) {
|
||||
document.getElementById('followersCount').textContent = data.followersCount;
|
||||
}
|
||||
|
||||
// Update button state
|
||||
await updateFollowButtonState();
|
||||
|
||||
// Show success message
|
||||
FitPub.showAlert(data.message, 'success');
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
FitPub.showAlert(errorData.error || 'Failed to update follow status', 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling follow:', error);
|
||||
FitPub.showAlert('An error occurred. Please try again.', 'danger');
|
||||
} finally {
|
||||
followBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
let currentPage = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue