WIP PWA for Grain
1import { auth } from './auth.js';
2import { recordCache } from './record-cache.js';
3import { parseTextToFacets } from '../lib/richtext.js';
4import { grainApi } from './grain-api.js';
5
6class MutationsService {
7 async createFavorite(galleryUri) {
8 const client = auth.getClient();
9 const result = await client.mutate(`
10 mutation CreateFavorite($input: SocialGrainFavoriteInput!) {
11 createSocialGrainFavorite(input: $input) { uri }
12 }
13 `, {
14 input: {
15 subject: galleryUri,
16 createdAt: new Date().toISOString()
17 }
18 });
19
20 return result.createSocialGrainFavorite.uri;
21 }
22
23 async deleteFavorite(favoriteUri) {
24 const client = auth.getClient();
25 const rkey = favoriteUri.split('/').pop();
26 await client.mutate(`
27 mutation DeleteFavorite($rkey: String!) {
28 deleteSocialGrainFavorite(rkey: $rkey) { uri }
29 }
30 `, { rkey });
31 }
32
33 async toggleFavorite(galleryUri, viewerHasFavorited, viewerFavoriteUri, currentCount) {
34 let newFavoriteUri = null;
35 let newCount = currentCount;
36
37 if (viewerHasFavorited) {
38 await this.deleteFavorite(viewerFavoriteUri);
39 newCount = Math.max(0, currentCount - 1);
40 } else {
41 newFavoriteUri = await this.createFavorite(galleryUri);
42 newCount = currentCount + 1;
43 }
44
45 const update = {
46 viewerHasFavorited: !viewerHasFavorited,
47 viewerFavoriteUri: newFavoriteUri,
48 favoriteCount: newCount
49 };
50
51 recordCache.set(galleryUri, update);
52
53 return update;
54 }
55
56 async createFollow(did) {
57 const client = auth.getClient();
58 const result = await client.mutate(`
59 mutation CreateFollow($input: SocialGrainGraphFollowInput!) {
60 createSocialGrainGraphFollow(input: $input) { uri }
61 }
62 `, {
63 input: {
64 subject: did,
65 createdAt: new Date().toISOString()
66 }
67 });
68
69 return result.createSocialGrainGraphFollow.uri;
70 }
71
72 async deleteFollow(followUri) {
73 const client = auth.getClient();
74 const rkey = followUri.split('/').pop();
75 await client.mutate(`
76 mutation DeleteFollow($rkey: String!) {
77 deleteSocialGrainGraphFollow(rkey: $rkey) { uri }
78 }
79 `, { rkey });
80 }
81
82 async toggleFollow(handle, did, viewerIsFollowing, viewerFollowUri, currentCount) {
83 let newFollowUri = null;
84 let newCount = currentCount;
85
86 if (viewerIsFollowing) {
87 await this.deleteFollow(viewerFollowUri);
88 newCount = Math.max(0, currentCount - 1);
89 } else {
90 newFollowUri = await this.createFollow(did);
91 newCount = currentCount + 1;
92 }
93
94 const update = {
95 viewerIsFollowing: !viewerIsFollowing,
96 viewerFollowUri: newFollowUri,
97 followerCount: newCount
98 };
99
100 recordCache.set(`profile:${handle}`, update);
101
102 return update;
103 }
104
105 async createComment(galleryUri, text, replyToUri = null, focusUri = null) {
106 const client = auth.getClient();
107
108 // Parse text for facets with handle resolution
109 const resolveHandle = async (handle) => grainApi.resolveHandle(handle);
110 const { facets } = await parseTextToFacets(text, resolveHandle);
111
112 const input = {
113 subject: galleryUri,
114 text,
115 createdAt: new Date().toISOString()
116 };
117
118 // Only include facets if we found any
119 if (facets && facets.length > 0) {
120 input.facets = facets;
121 }
122
123 if (replyToUri) {
124 input.replyTo = replyToUri;
125 }
126
127 if (focusUri) {
128 input.focus = focusUri;
129 }
130
131 const result = await client.mutate(`
132 mutation CreateComment($input: SocialGrainCommentInput!) {
133 createSocialGrainComment(input: $input) { uri }
134 }
135 `, { input });
136
137 return result.createSocialGrainComment.uri;
138 }
139
140 async deleteComment(commentUri) {
141 const client = auth.getClient();
142 const rkey = commentUri.split('/').pop();
143 await client.mutate(`
144 mutation DeleteComment($rkey: String!) {
145 deleteSocialGrainComment(rkey: $rkey) { uri }
146 }
147 `, { rkey });
148 }
149
150 async uploadBlob(base64Data, mimeType = 'image/jpeg') {
151 const client = auth.getClient();
152 const result = await client.mutate(`
153 mutation UploadBlob($data: String!, $mimeType: String!) {
154 uploadBlob(data: $data, mimeType: $mimeType) {
155 ref
156 mimeType
157 size
158 }
159 }
160 `, { data: base64Data, mimeType });
161
162 return result.uploadBlob;
163 }
164
165 async updateAvatar(dataUrl, profile) {
166 const client = auth.getClient();
167
168 // Upload the blob (already resized by crop component)
169 const base64Data = dataUrl.split(',')[1];
170 const blob = await this.uploadBlob(base64Data, 'image/jpeg');
171
172 if (!blob) {
173 throw new Error('Failed to upload avatar');
174 }
175
176 // Build input with all profile fields (update requires full object)
177 const input = {
178 displayName: profile.displayName || null,
179 description: profile.description || null,
180 createdAt: profile.createdAt || new Date().toISOString(),
181 avatar: {
182 $type: 'blob',
183 ref: { $link: blob.ref },
184 mimeType: blob.mimeType,
185 size: blob.size
186 }
187 };
188
189 // Update profile
190 await client.mutate(`
191 mutation UpdateProfile($rkey: String!, $input: SocialGrainActorProfileInput!) {
192 updateSocialGrainActorProfile(rkey: $rkey, input: $input) {
193 uri
194 }
195 }
196 `, { rkey: 'self', input });
197
198 // Refresh user data
199 await auth.refreshUser();
200 }
201
202 async updateProfile(input) {
203 const client = auth.getClient();
204
205 await client.mutate(`
206 mutation UpdateProfile($rkey: String!, $input: SocialGrainActorProfileInput!) {
207 updateSocialGrainActorProfile(rkey: $rkey, input: $input) {
208 uri
209 }
210 }
211 `, { rkey: 'self', input });
212
213 await auth.refreshUser();
214 }
215
216 async createEmptyProfile() {
217 return this.updateProfile({
218 createdAt: new Date().toISOString()
219 });
220 }
221
222 async createReport(subjectUri, reasonType, reason = null) {
223 const client = auth.getClient();
224 const result = await client.mutate(`
225 mutation CreateReport($subjectUri: String!, $reasonType: ReportReasonType!, $reason: String) {
226 createReport(subjectUri: $subjectUri, reasonType: $reasonType, reason: $reason) {
227 id
228 status
229 createdAt
230 }
231 }
232 `, { subjectUri, reasonType, reason });
233
234 return result.createReport;
235 }
236}
237
238export const mutations = new MutationsService();