feat(users): implement user following system and following feed

Add follow/unfollow actions and social stats to user profiles
Introduce Following feed scope in Life view
Add notifications for new followers
This commit is contained in:
2026-05-04 15:49:57 +08:00
parent 016364a8b8
commit 8cb8190554
11 changed files with 472 additions and 18 deletions

View File

@@ -64,6 +64,7 @@ import {
exportAdminData,
fetchPokemonData,
fetchPokemonImageOptions,
followUser,
getAdminDataToolsSummary,
getAncientArtifact,
getHabitat,
@@ -81,6 +82,7 @@ import {
listConfig,
listDailyChecklistItems,
listHabitats,
listFollowingLifePosts,
listItems,
listLifeComments,
listLanguages,
@@ -115,6 +117,7 @@ import {
updateLifePost,
updatePokemon,
updateRecipe,
unfollowUser,
wipeAdminData
} from './queries.ts';
import {
@@ -1184,7 +1187,30 @@ app.get('/api/daily-checklist', async (request) => listDailyChecklistItems(reque
app.get('/api/users/:id/profile', async (request, reply) => {
const { id } = request.params as { id: string };
const profile = await getPublicUserProfile(Number(id));
const user = await optionalUser(request);
const profile = await getPublicUserProfile(Number(id), user?.id ?? null);
return profile ? { profile } : notFound(reply, request);
});
app.put('/api/users/:id/follow', async (request, reply) => {
const user = await requirePermissionWithRateLimits(request, reply, 'users.follow', 'communityReaction');
if (!user) {
return;
}
const { id } = request.params as { id: string };
const profile = await followUser(user.id, Number(id));
return profile ? { profile } : notFound(reply, request);
});
app.delete('/api/users/:id/follow', async (request, reply) => {
const user = await requirePermissionWithRateLimits(request, reply, 'users.follow', 'communityReaction');
if (!user) {
return;
}
const { id } = request.params as { id: string };
const profile = await unfollowUser(user.id, Number(id));
return profile ? { profile } : notFound(reply, request);
});
@@ -1239,6 +1265,20 @@ app.get('/api/life-posts', async (request) => {
);
});
app.get('/api/life-posts/following', async (request, reply) => {
const user = await requireVerifiedUser(request, reply);
if (!user) {
return;
}
const canViewAll = userHasPermission(user, 'life.posts.update-any') || userHasPermission(user, 'life.posts.delete-any');
return listFollowingLifePosts(
user.id,
request.query as Record<string, string | string[] | undefined>,
requestLocale(request),
canViewAll
);
});
app.get('/api/life-posts/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const user = await optionalUser(request);