Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platforms/pictique/api/src/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class UserController {
return res.status(400).json({ error: "User ID is required" });
}

const profile = await this.userService.getProfileById(id);
const profile = await this.userService.getProfileById(id, req.user?.id);
if (!profile) {
return res.status(404).json({ error: "User not found" });
}
Expand Down
2 changes: 1 addition & 1 deletion platforms/pictique/api/src/database/entities/Chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class Chat {
@OneToMany(() => Message, (e) => e.chat)
messages!: Message[];

@ManyToMany(() => User)
@ManyToMany(() => User, (user) => user.chats)
@JoinTable({
name: "chat_participants",
joinColumn: { name: "chat_id", referencedColumnName: "id" },
Expand Down
90 changes: 67 additions & 23 deletions platforms/pictique/api/src/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,43 +354,78 @@ export class UserService {
};

followUser = async (followerId: string, followingId: string) => {
const follower = await this.userRepository.findOne({
where: { id: followerId },
relations: ["following"],
});

const following = await this.userRepository.findOne({
where: { id: followingId },
});

if (!follower || !following) {
throw new Error("User not found");
if (followerId === followingId) {
throw new Error("Cannot follow yourself");
}

if (!follower.following) {
follower.following = [];
}
// Both sides are read, checked, and (if needed) written inside one
// transaction so a failure partway through can't leave one join
// table updated and the other not.
await this.userRepository.manager.transaction(async (manager) => {
const follower = await manager.findOne(User, {
where: { id: followerId },
relations: ["following"],
});

const following = await manager.findOne(User, {
where: { id: followingId },
relations: ["followers"],
});

if (!follower || !following) {
throw new Error("User not found");
}

if (!follower.following) {
follower.following = [];
}
if (!following.followers) {
following.followers = [];
}

// The two sides are backed by separate join tables and can drift
// out of sync (e.g. a prior write that only landed on one side),
// so each side is checked and repaired independently rather than
// assuming one side being present implies the other is too.
const missingFromFollowing = !follower.following.some(
(user) => user.id === followingId,
);
const missingFromFollowers = !following.followers.some(
(user) => user.id === followerId,
);

// Check if already following
if (follower.following.some((user) => user.id === followingId)) {
return follower;
}
if (missingFromFollowing) {
follower.following.push(following);
await manager.save(follower);
}
if (missingFromFollowers) {
following.followers.push(follower);
await manager.save(following);
}
});

follower.following.push(following);
return await this.userRepository.save(follower);
// follower.following and following.followers now reference each
// other, so the loaded entities aren't safe to JSON-serialize
// directly. Return the freshly updated target profile instead,
// using the same safe shape getProfileById already builds.
return this.getProfileById(followingId, followerId);
};

async getProfileById(userId: string) {
async getProfileById(userId: string, viewerId?: string) {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: {
followers: true,
following: true,
},
select: {
id: true,
handle: true,
name: true,
avatarUrl: true,
followers: true,
following: true,
description: true,
followers: { id: true },
following: { id: true },
},
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand All @@ -402,8 +437,17 @@ export class UserService {
order: { createdAt: "DESC" },
});

const followers = user.followers ?? [];
const following = user.following ?? [];

return {
...user,
username: user.handle,
followers: followers.length,
following: following.length,
isFollowing: viewerId
? followers.some((follower) => follower.id === viewerId)
: false,
totalPosts: posts.length,
posts: posts.map((post) => ({
id: post.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface {
setTimeout(async () => {
try {
let globalId = await this.adapter.mappingDb.getGlobalId(
entity.id
parentEntity.id
);
globalId = globalId ?? "";

Expand All @@ -348,7 +348,7 @@ export class PostgresSubscriber implements EntitySubscriberInterface {
console.log(
"sending packet for global Id",
globalId,
entity.id
parentEntity.id
);

const tableName = `${junctionInfo.entity.toLowerCase()}s`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@
handleFollow,
handleSinglePost,
handleMessage,
isFollowing = $bindable(false),
didFollowed = $bindable(false)
followPending = false
}: {
variant: 'user' | 'other';
profileData: userProfile;
handleSinglePost: (post: PostData) => void;
handleFollow: () => Promise<void>;
handleMessage: () => Promise<void>;
isFollowing: boolean;
didFollowed: boolean;
followPending?: boolean;
} = $props();

let imgPosts = $derived(profileData.posts.filter((e) => e.imgUris && e.imgUris.length > 0));
Expand Down Expand Up @@ -59,15 +57,15 @@
variant={'primary'}
size="sm"
callback={wrappedFollow}
class="min-w-[110px] transition-all duration-500 {didFollowed
class="min-w-[110px] transition-all duration-500 {profileData.isFollowing
? 'opacity-80'
: ''}"
>
<div class="flex items-center justify-center gap-2">
{#if didFollowed}
{#if profileData.isFollowing}
<HugeiconsIcon icon={Tick01Icon} size={16} />
<span>Following</span>
{:else if isFollowing}
{:else if followPending}
<span class="flex gap-0.5">
<span class="animate-bounce">.</span>
<span class="animate-bounce [animation-delay:0.2s]">.</span>
Expand Down Expand Up @@ -96,11 +94,11 @@
<p class="text-gray-600">Posts</p>
</div>
<div>
<p class="font-semibold">{0}</p>
<p class="font-semibold">{profileData.followers}</p>
<p class="text-gray-600">Followers</p>
</div>
<div>
<p class="font-semibold">{0}</p>
<p class="font-semibold">{profileData.following}</p>
<p class="text-gray-600">Following</p>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion platforms/pictique/client/src/lib/stores/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const searchUsers = async (query: string) => {

export const followUser = async (followingId: string) => {
try {
await apiClient.post('/api/users/follow', { followingId });
await apiClient.post(`/api/users/${followingId}/follow`);
return true;
} catch (err) {
console.error('Failed to follow user:', err);
Expand Down
1 change: 1 addition & 0 deletions platforms/pictique/client/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type userProfile = {
totalPosts: number;
followers: number;
following: number;
isFollowing: boolean;
posts: PostData[];
username: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
let error = $state<string | null>(null);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let loading = $state(true);
let ownerId: string | null = $derived(getAuthId());
let isFollowing = $state(false);
let didFollowed = $state(false);
let followPending = $state(false);
let ownerProfile = $derived.by(async () => {
if (ownerId) {
const response = await apiClient.get<userProfile>(`/api/users/${ownerId}`);
Expand All @@ -51,21 +50,15 @@
}

async function handleFollow() {
if (!profile || profile.isFollowing || followPending) return;
followPending = true;
try {
isFollowing = true;
const response = await apiClient.post(`/api/users/${profileId}/follow`);
if (response) {
didFollowed = true;
setTimeout(async () => {
await fetchProfile();
didFollowed = false;
}, 1000);
}
const response = await apiClient.post<userProfile>(`/api/users/${profileId}/follow`);
profile = response.data;
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to follow user';
didFollowed = false;
} finally {
isFollowing = false;
followPending = false;
}
}

Expand Down Expand Up @@ -106,8 +99,7 @@
</div>
{:else if profile}
<Profile
bind:isFollowing
bind:didFollowed
{followPending}
variant={ownerId === profileId ? 'user' : 'other'}
profileData={profile}
handleSinglePost={(post) => handlePostClick(post)}
Expand Down
Loading