RecoveryView.svelte raw
1 <script>
2 export let recoverySelectedKind = null;
3 export let recoveryCustomKind = "";
4 export let isLoadingRecovery = false;
5 export let recoveryEvents = [];
6 export let recoveryHasMore = false;
7
8 import { createEventDispatcher } from "svelte";
9 const dispatch = createEventDispatcher();
10
11 const replaceableKinds = [
12 { value: 0, label: "Profile (0)" },
13 { value: 3, label: "Contacts (3)" },
14 { value: 10000, label: "Mute List (10000)" },
15 { value: 10001, label: "Pin List (10001)" },
16 { value: 10002, label: "Relay List (10002)" },
17 { value: 30000, label: "Categorized People (30000)" },
18 { value: 30001, label: "Categorized Bookmarks (30001)" },
19 { value: 30008, label: "Profile Badges (30008)" },
20 { value: 30009, label: "Badge Definition (30009)" },
21 { value: 30017, label: "Create or update a stall (30017)" },
22 { value: 30018, label: "Create or update a product (30018)" },
23 { value: 30023, label: "Long-form Content (30023)" },
24 { value: 30024, label: "Draft Long-form Content (30024)" },
25 { value: 30078, label: "Application-specific Data (30078)" },
26 { value: 30311, label: "Live Event (30311)" },
27 { value: 30315, label: "User Statuses (30315)" },
28 { value: 30402, label: "Classified Listing (30402)" },
29 { value: 30403, label: "Draft Classified Listing (30403)" },
30 { value: 31922, label: "Date-Based Calendar Event (31922)" },
31 { value: 31923, label: "Time-Based Calendar Event (31923)" },
32 { value: 31924, label: "Calendar (31924)" },
33 { value: 31925, label: "Calendar Event RSVP (31925)" },
34 { value: 31989, label: "Handler recommendation (31989)" },
35 { value: 31990, label: "Handler information (31990)" },
36 { value: 34550, label: "Community Definition (34550)" },
37 ];
38
39 function selectRecoveryKind() {
40 dispatch("selectRecoveryKind");
41 }
42
43 function handleCustomKindInput() {
44 dispatch("handleCustomKindInput");
45 }
46
47 function loadRecoveryEvents() {
48 dispatch("loadRecoveryEvents");
49 }
50
51 function repostEventToAll(event) {
52 dispatch("repostEventToAll", event);
53 }
54
55 function repostEvent(event) {
56 dispatch("repostEvent", event);
57 }
58
59 function copyEventToClipboard(event, e) {
60 dispatch("copyEventToClipboard", { event, e });
61 }
62
63 function isCurrentVersion(event) {
64 // This logic would need to be passed from parent or implemented here
65 // For now, just return false for old versions
66 return false;
67 }
68 </script>
69
70 <div class="recovery-tab">
71 <div>
72 <h3>Event Recovery</h3>
73 <p>Search and recover old versions of replaceable events</p>
74 </div>
75
76 <div class="recovery-controls-card">
77 <div class="recovery-controls">
78 <div class="kind-selector">
79 <label for="recovery-kind">Select Event Kind:</label>
80 <select
81 id="recovery-kind"
82 bind:value={recoverySelectedKind}
83 on:change={selectRecoveryKind}
84 >
85 <option value={null}>Choose a replaceable kind...</option>
86 {#each replaceableKinds as kind}
87 <option value={kind.value}>{kind.label}</option>
88 {/each}
89 </select>
90 </div>
91
92 <div class="custom-kind-input">
93 <label for="custom-kind">Or enter custom kind number:</label>
94 <input
95 id="custom-kind"
96 type="number"
97 bind:value={recoveryCustomKind}
98 on:input={handleCustomKindInput}
99 placeholder="e.g., 10001"
100 min="0"
101 />
102 </div>
103 </div>
104 </div>
105
106 {#if (recoverySelectedKind !== null && recoverySelectedKind !== undefined && recoverySelectedKind >= 0) || (recoveryCustomKind !== "" && parseInt(recoveryCustomKind) >= 0)}
107 <div class="recovery-results">
108 {#if isLoadingRecovery}
109 <div class="loading">Loading events...</div>
110 {:else if recoveryEvents.length === 0}
111 <div class="no-events">No events found for this kind</div>
112 {:else}
113 <div class="events-list">
114 {#each recoveryEvents as event}
115 {@const isCurrent = isCurrentVersion(event)}
116 <div class="event-item" class:old-version={!isCurrent}>
117 <div class="event-header">
118 <div class="event-header-left">
119 <span class="event-kind">
120 {#if isCurrent}
121 Current Version{/if}</span
122 >
123 <span class="event-timestamp">
124 {new Date(
125 event.created_at * 1000,
126 ).toLocaleString()}
127 </span>
128 </div>
129 <div class="event-header-actions">
130 {#if !isCurrent}
131 <button
132 class="repost-all-button"
133 on:click={() =>
134 repostEventToAll(event)}
135 >
136 🌐 Repost to All
137 </button>
138 <button
139 class="repost-button"
140 on:click={() => repostEvent(event)}
141 >
142 🔄 Repost
143 </button>
144 {/if}
145 <button
146 class="copy-json-btn"
147 on:click|stopPropagation={(e) =>
148 copyEventToClipboard(event, e)}
149 >
150 📋 Copy JSON
151 </button>
152 </div>
153 </div>
154
155 <div class="event-content">
156 <pre class="event-json">{JSON.stringify(
157 event,
158 null,
159 2,
160 )}</pre>
161 </div>
162 </div>
163 {/each}
164 </div>
165
166 {#if recoveryHasMore}
167 <button
168 class="load-more"
169 on:click={loadRecoveryEvents}
170 disabled={isLoadingRecovery}
171 >
172 Load More Events
173 </button>
174 {/if}
175 {/if}
176 </div>
177 {/if}
178 </div>
179
180 <style>
181 .recovery-tab {
182 width: 100%;
183 max-width: 1200px;
184 margin: 0;
185 padding: 20px;
186 background: var(--header-bg);
187 color: var(--text-color);
188 border-radius: 8px;
189 box-sizing: border-box;
190 }
191
192 .recovery-tab h3 {
193 margin: 0 0 0.5rem 0;
194 color: var(--text-color);
195 font-size: 1.5rem;
196 font-weight: 600;
197 }
198
199 .recovery-tab p {
200 margin: 0 0 1.5rem 0;
201 color: var(--text-color);
202 opacity: 0.8;
203 line-height: 1.4;
204 }
205
206 .recovery-controls-card {
207 background-color: var(--card-bg);
208 border-radius: 0.5em;
209 padding: 1em;
210 margin-bottom: 1.5rem;
211 }
212
213 .recovery-controls {
214 display: flex;
215 flex-direction: column;
216 gap: 1em;
217 }
218
219 .kind-selector,
220 .custom-kind-input {
221 display: flex;
222 flex-direction: column;
223 gap: 0.5em;
224 }
225
226 .kind-selector label,
227 .custom-kind-input label {
228 font-weight: 600;
229 color: var(--text-color);
230 }
231
232 .kind-selector select,
233 .custom-kind-input input {
234 padding: 0.5em;
235 border: 1px solid var(--border-color);
236 border-radius: 4px;
237 background: var(--input-bg);
238 color: var(--input-text-color);
239 font-size: 0.9em;
240 }
241
242 .recovery-results {
243 margin-top: 1.5rem;
244 }
245
246 .loading,
247 .no-events {
248 text-align: center;
249 padding: 2em;
250 color: var(--text-color);
251 opacity: 0.7;
252 }
253
254 .events-list {
255 display: flex;
256 flex-direction: column;
257 gap: 1em;
258 }
259
260 .event-item {
261 background: var(--card-bg);
262 border: 1px solid var(--border-color);
263 border-radius: 8px;
264 padding: 1em;
265 }
266
267 .event-item.old-version {
268 border-color: var(--warning);
269 background: var(--warning-bg);
270 }
271
272 .event-header {
273 display: flex;
274 justify-content: space-between;
275 align-items: center;
276 margin-bottom: 1em;
277 flex-wrap: wrap;
278 gap: 1em;
279 }
280
281 .event-header-left {
282 display: flex;
283 flex-direction: column;
284 gap: 0.25em;
285 }
286
287 .event-kind {
288 font-weight: 600;
289 color: var(--primary);
290 }
291
292 .event-timestamp {
293 font-size: 0.9em;
294 color: var(--text-color);
295 opacity: 0.7;
296 }
297
298 .event-header-actions {
299 display: flex;
300 gap: 0.5em;
301 flex-wrap: wrap;
302 }
303
304 .repost-all-button,
305 .repost-button,
306 .copy-json-btn {
307 background: var(--accent-color);
308 color: var(--accent-hover-color);
309 border: none;
310 padding: 0.5em;
311 border-radius: 0.5em;
312 cursor: pointer;
313 font-size: 0.8em;
314 transition: background-color 0.2s;
315 }
316
317 .repost-all-button:hover,
318 .repost-button:hover,
319 .copy-json-btn:hover {
320 background: var(--accent-hover-color);
321 }
322
323 .event-content {
324 margin-top: 1em;
325 }
326
327 .event-json {
328 background: var(--code-bg);
329 padding: 1em;
330 border: 0;
331 font-size: 0.8em;
332 line-height: 1.4;
333 overflow-x: auto;
334 margin: 0;
335 color: var(--code-text);
336 }
337
338 .load-more {
339 width: 100%;
340 padding: 12px;
341 background: var(--primary);
342 color: var(--text-color);
343 border: none;
344 border-radius: 4px;
345 cursor: pointer;
346 font-size: 1em;
347 margin-top: 20px;
348 transition: background 0.2s ease;
349 }
350
351 .load-more:hover:not(:disabled) {
352 background: var(--accent-hover-color);
353 }
354
355 .load-more:disabled {
356 opacity: 0.6;
357 cursor: not-allowed;
358 }
359 </style>
360