beatufitull front end for ozone modration ,, wit catpucoin and ebergarden !
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add acknoledge option

+249 -135
+72
src/components/ui/DropdownList.svelte
··· 1 + <script lang="ts"> 2 + import { ChevronDownIcon } from 'lucide-svelte'; 3 + import { inputAccentColor, type InputAccentColor } from '$lib/stores/ui'; 4 + import type { DropdownOption } from '$lib/types'; 5 + 6 + export let id: string | undefined = undefined; 7 + export let value = ''; 8 + export let options: DropdownOption[] = []; 9 + export let placeholder = 'Select an option'; 10 + export let disabled = false; 11 + export let label = ''; 12 + export let helpText = ''; 13 + export let error = ''; 14 + export let color: InputAccentColor | null = null; 15 + export let className = ''; 16 + 17 + $: activeColor = color ?? $inputAccentColor; 18 + $: colorClass = 19 + activeColor === 'mauve' 20 + ? 'focus:border-ctp-mauve focus:ring-2 focus:ring-ctp-mauve' 21 + : activeColor === 'lavender' 22 + ? 'focus:border-ctp-lavender focus:ring-2 focus:ring-ctp-lavender' 23 + : 'focus:border-ctp-sapphire focus:ring-2 focus:ring-ctp-sapphire'; 24 + 25 + $: selectClass = [ 26 + 'w-full appearance-none rounded-lg border border-ctp-surface1 bg-ctp-surface0 px-4 py-2 pr-10 text-ctp-text', 27 + 'focus:outline-none', 28 + colorClass, 29 + error ? 'border-ctp-red focus:border-ctp-red focus:ring-ctp-red' : '', 30 + disabled ? 'cursor-not-allowed bg-ctp-surface1 opacity-50' : 'cursor-pointer', 31 + className 32 + ] 33 + .filter(Boolean) 34 + .join(' '); 35 + </script> 36 + 37 + <div class="space-y-1"> 38 + {#if label} 39 + <label for={id} class="block text-sm font-medium text-ctp-subtext0">{label}</label> 40 + {/if} 41 + 42 + <div class="relative"> 43 + <select 44 + {id} 45 + bind:value 46 + {disabled} 47 + class={selectClass} 48 + on:change 49 + on:input 50 + on:focus 51 + on:blur 52 + on:keydown 53 + {...$$restProps} 54 + > 55 + {#if placeholder} 56 + <option value="" disabled hidden>{placeholder}</option> 57 + {/if} 58 + {#each options as option} 59 + <option value={option.value} disabled={option.disabled}>{option.label}</option> 60 + {/each} 61 + </select> 62 + <div class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-ctp-subtext1"> 63 + <ChevronDownIcon class="h-4 w-4" /> 64 + </div> 65 + </div> 66 + 67 + {#if error} 68 + <p class="text-xs text-ctp-red">{error}</p> 69 + {:else if helpText} 70 + <p class="text-xs text-ctp-subtext1">{helpText}</p> 71 + {/if} 72 + </div>
+129 -35
src/components/view/Actions.svelte
··· 3 3 import TextField from '../ui/TextField.svelte'; 4 4 import Button from '../ui/Button.svelte'; 5 5 import Checkbox from '../ui/Checkbox.svelte'; 6 + import DropdownList from '../ui/DropdownList.svelte'; 6 7 import { createQuery } from '@tanstack/svelte-query'; 7 8 import { moderationConfigQueryOptions } from '$lib/queries/moderation'; 8 9 import { session } from '$lib/stores/auth'; 10 + import type { DropdownOption } from '$lib/types'; 11 + import type { RepoRef } from '@atproto/api/dist/client/types/com/atproto/admin/defs'; 12 + import type { $Typed, ComAtprotoRepoStrongRef } from '@atproto/api'; 9 13 10 14 let { 11 15 initialSelectedLabelVals = [], 12 - onSubmit 16 + onRefresh, 17 + ref 13 18 }: { 14 19 initialSelectedLabelVals?: string[]; 15 - onSubmit: ( 16 - reason: string, 17 - labels: { 18 - createLabelVals: string[]; 19 - negateLabelVals: string[]; 20 - } 21 - ) => Promise<void>; 20 + ref: $Typed<RepoRef> | $Typed<ComAtprotoRepoStrongRef.Main>; 21 + onRefresh: () => Promise<void>; 22 22 } = $props(); 23 23 const labelerConfig = createQuery(() => moderationConfigQueryOptions($session?.agent)); 24 + const labelActionOptions = [ 25 + { value: 'labels', label: 'Apply Labels' }, 26 + { value: 'acknowledge', label: 'Acknowledge' } 27 + ] as const satisfies readonly DropdownOption[]; 28 + export const labelActionOptionsMutatable: DropdownOption[] = labelActionOptions.map((o) => ({ 29 + ...o 30 + })); 31 + 32 + type LabelAction = (typeof labelActionOptions)[number]['value']; 24 33 25 34 let selectedLabelVals = $state(new Set<string>()); 26 35 let reason = $state(''); 27 - let labelSubmitting = $state(false); 36 + let isSubmitting = $state(false); 37 + let labelAction = $state<LabelAction>('labels'); 28 38 29 39 $effect(() => { 30 40 selectedLabelVals = new Set(initialSelectedLabelVals); ··· 57 67 reason = newReason; 58 68 } 59 69 70 + async function submitLabels( 71 + reason: string, 72 + { createLabelVals, negateLabelVals }: { createLabelVals: string[]; negateLabelVals: string[] } 73 + ) { 74 + const agent = $session?.agent; 75 + if (!agent) { 76 + throw new Error('No active session'); 77 + } 78 + 79 + if (createLabelVals.length === 0 && negateLabelVals.length === 0) { 80 + return; 81 + } 82 + 83 + const result = await agent.tools.ozone.moderation.emitEvent({ 84 + subject: ref, 85 + modTool: { 86 + $type: 'tools.ozone.moderation.defs#modTool', 87 + name: 'meowzone' 88 + }, 89 + event: { 90 + $type: 'tools.ozone.moderation.defs#modEventLabel', 91 + negateLabelVals, 92 + createLabelVals, 93 + comment: reason 94 + }, 95 + createdBy: agent.assertDid 96 + }); 97 + 98 + if (!result.success) { 99 + throw new Error('Failed to submit labels'); 100 + } else { 101 + onRefresh(); 102 + } 103 + } 104 + 105 + async function acknowledge(reason: string) { 106 + const agent = $session?.agent; 107 + if (!agent) { 108 + throw new Error('No active session'); 109 + } 110 + 111 + const result = await agent.tools.ozone.moderation.emitEvent({ 112 + subject: ref, 113 + modTool: { 114 + $type: 'tools.ozone.moderation.defs#modTool', 115 + name: 'meowzone' 116 + }, 117 + event: { 118 + $type: 'tools.ozone.moderation.defs#modEventAcknowledge', 119 + comment: reason 120 + }, 121 + createdBy: agent.assertDid 122 + }); 123 + 124 + if (!result.success) { 125 + throw new Error('Failed to submit acknowledge event'); 126 + } else { 127 + onRefresh(); 128 + } 129 + } 130 + 131 + function canSubmit(): boolean { 132 + switch (labelAction) { 133 + case 'labels': { 134 + return hasLabelChanges; 135 + } 136 + case 'acknowledge': { 137 + return true; 138 + } 139 + default: { 140 + return false; 141 + } 142 + } 143 + } 144 + 60 145 async function onSubmitClick() { 61 - if (labelSubmitting) return; 62 - labelSubmitting = true; 146 + if (isSubmitting) return; 147 + isSubmitting = true; 63 148 try { 64 - await onSubmit(reason, getLabelDiff()); 149 + if (labelAction === 'labels') { 150 + await submitLabels(reason, getLabelDiff()); 151 + } else if (labelAction === 'acknowledge') { 152 + await acknowledge(reason); 153 + } 65 154 } catch (error) { 66 - console.error('Failed to submit labels:', error); 155 + console.error('Failed to submit:', error); 67 156 } finally { 68 - labelSubmitting = false; 157 + isSubmitting = false; 69 158 } 70 159 } 71 160 </script> 72 161 73 - <h3 class="text-md mt-4 text-ctp-text">Labels</h3> 74 162 {#if labelerConfig.data?.labels} 75 163 <div class="mt-2 space-y-3"> 76 - <div class="flex items-center justify-between"> 77 - <p class="text-xs text-ctp-subtext0">Select labels to apply</p> 78 - <p class="text-xs text-ctp-subtext1">{selectedLabelVals.size} selected</p> 79 - </div> 80 - <div class="flex flex-wrap gap-2 rounded-lg"> 81 - {#each labelerConfig.data?.labels as label} 82 - {@const isSelected = selectedLabelVals.has(label.val)} 83 - <div 84 - class={`rounded-lg border border-ctp-surface1 bg-ctp-surface0 px-2.5 py-1.5 transition-colors hover:border-ctp-surface2`} 85 - > 86 - <Checkbox 87 - checked={isSelected} 88 - on:change={() => onToggleLabel(label.val)} 89 - label={label.name} 90 - /> 91 - </div> 92 - {/each} 93 - </div> 164 + <DropdownList 165 + label="Action" 166 + value={labelAction} 167 + options={labelActionOptionsMutatable} 168 + on:change={(event) => { 169 + labelAction = (event.currentTarget as HTMLSelectElement).value as LabelAction; 170 + }} 171 + /> 172 + {#if labelAction == 'labels'} 173 + <div class="flex flex-wrap gap-2 rounded-lg"> 174 + {#each labelerConfig.data?.labels as label} 175 + {@const isSelected = selectedLabelVals.has(label.val)} 176 + <div 177 + class={`rounded-lg border border-ctp-surface1 bg-ctp-surface0 px-2.5 py-1.5 transition-colors hover:border-ctp-surface2`} 178 + > 179 + <Checkbox 180 + checked={isSelected} 181 + on:change={() => onToggleLabel(label.val)} 182 + label={label.name} 183 + /> 184 + </div> 185 + {/each} 186 + </div> 187 + {/if} 94 188 <TextField 95 189 label="Reason" 96 190 placeholder="Enter reason" ··· 100 194 <Button 101 195 variant="primary" 102 196 fullWidth={true} 103 - disabled={labelSubmitting || !hasLabelChanges} 197 + disabled={isSubmitting || !canSubmit()} 104 198 on:click={onSubmitClick} 105 199 > 106 - {#if labelSubmitting} 200 + {#if isSubmitting} 107 201 <LoaderCircle class="h-4 w-4 animate-spin" /> 108 202 {/if} 109 203 Submit
+9 -37
src/components/view/Post.svelte
··· 111 111 activePanel = 'post'; 112 112 } 113 113 }); 114 - 115 - async function submitLabels( 116 - reason: string, 117 - { createLabelVals, negateLabelVals }: { createLabelVals: string[]; negateLabelVals: string[] } 118 - ) { 119 - const agent = $session?.agent; 120 - if (!agent) { 121 - throw new Error('No active session'); 122 - } 123 - if (createLabelVals.length === 0 && negateLabelVals.length === 0) { 124 - return; 125 - } 126 - const result = await agent.tools.ozone.moderation.emitEvent({ 127 - subject: { 128 - $type: 'com.atproto.repo.strongRef', 129 - uri: uri.toString() 130 - }, 131 - modTool: { 132 - $type: 'tools.ozone.moderation.defs#modTool', 133 - name: 'meowzone' 134 - }, 135 - event: { 136 - $type: 'tools.ozone.moderation.defs#modEventLabel', 137 - negateLabelVals, 138 - createLabelVals, 139 - comment: reason 140 - }, 141 - createdBy: agent.assertDid 142 - }); 143 - if (!result.success) { 144 - throw new Error('Failed to submit labels'); 145 - } else { 146 - await postQuery.refetch(); 147 - await eventsQuery.refetch(); 148 - } 149 - } 150 114 </script> 151 115 152 116 {#snippet postPanel()} ··· 189 153 190 154 <Actions 191 155 initialSelectedLabelVals={postQuery.data.labels?.map((label) => label.val) ?? []} 192 - onSubmit={(reason, labels) => submitLabels(reason, labels)} 156 + ref={{ 157 + $type: 'com.atproto.repo.strongRef', 158 + uri: uri.toString(), 159 + cid: postQuery.data.cid 160 + }} 161 + onRefresh={async () => { 162 + await postQuery.refetch(); 163 + await eventsQuery.refetch(); 164 + }} 193 165 /> 194 166 {:else if profileQuery.isError || postQuery.isError} 195 167 <p class="text-sm text-ctp-red">
+32 -62
src/components/view/User.svelte
··· 100 100 activePanel = 'profile'; 101 101 } 102 102 }); 103 - 104 - async function submitLabels( 105 - reason: string, 106 - { createLabelVals, negateLabelVals }: { createLabelVals: string[]; negateLabelVals: string[] } 107 - ) { 108 - const agent = $session?.agent; 109 - if (!agent) { 110 - throw new Error('No active session'); 111 - } 112 - 113 - if (createLabelVals.length === 0 && negateLabelVals.length === 0) { 114 - return; 115 - } 116 - 117 - const result = await agent.tools.ozone.moderation.emitEvent({ 118 - subject: { 119 - $type: 'com.atproto.admin.defs#repoRef', 120 - did: actor 121 - }, 122 - modTool: { 123 - $type: 'tools.ozone.moderation.defs#modTool', 124 - name: 'meowzone' 125 - }, 126 - event: { 127 - $type: 'tools.ozone.moderation.defs#modEventLabel', 128 - negateLabelVals, 129 - createLabelVals, 130 - comment: reason 131 - }, 132 - createdBy: agent.assertDid 133 - }); 134 - 135 - if (!result.success) { 136 - throw new Error('Failed to submit labels'); 137 - } else { 138 - await userQuery.refetch(); 139 - await eventsQuery.refetch(); 140 - } 141 - } 142 103 </script> 143 104 144 105 {#snippet profilePanel()} ··· 148 109 <LoaderCircle class="h-8 w-8 animate-spin text-ctp-text" /> 149 110 </div> 150 111 {:else if userQuery.data} 151 - <div class="mb-4 flex items-center gap-4"> 152 - <img src={userQuery.data.profile.avatar} alt="Avatar" class="size-12 rounded-full" /> 153 - <div> 154 - <h2 class="text-xl text-ctp-text">{userQuery.data.profile.displayName}</h2> 155 - <p class="text-sm text-ctp-subtext0">@{userQuery.data.profile.handle}</p> 156 - </div> 157 - <div class="ml-auto flex items-center gap-2"> 158 - <a 159 - href={`https://catsky.social/profile/${actor}`} 160 - target="_blank" 161 - rel="noopener noreferrer" 162 - > 163 - <img 164 - src={catsky} 165 - alt="Catsky" 166 - class="size-6 opacity-30 transition-opacity hover:opacity-100" 167 - /> 168 - </a> 169 - <a href={`https://bsky.app/profile/${actor}`} target="_blank" rel="noopener noreferrer"> 170 - <Bluesky className="size-6 fill-ctp-blue/30 hover:fill-ctp-blue transition-colors" /> 171 - </a> 112 + <div class="mb-4 rounded-lg border border-ctp-surface1 px-4 py-3"> 113 + <div class="mb-4 flex items-center gap-4"> 114 + <img src={userQuery.data.profile.avatar} alt="Avatar" class="size-12 rounded-full" /> 115 + <div> 116 + <h2 class="text-xl text-ctp-text">{userQuery.data.profile.displayName}</h2> 117 + <p class="text-sm text-ctp-subtext0">@{userQuery.data.profile.handle}</p> 118 + </div> 119 + <div class="ml-auto flex items-center gap-2"> 120 + <a 121 + href={`https://catsky.social/profile/${actor}`} 122 + target="_blank" 123 + rel="noopener noreferrer" 124 + > 125 + <img 126 + src={catsky} 127 + alt="Catsky" 128 + class="size-6 opacity-30 transition-opacity hover:opacity-100" 129 + /> 130 + </a> 131 + <a href={`https://bsky.app/profile/${actor}`} target="_blank" rel="noopener noreferrer"> 132 + <Bluesky className="size-6 fill-ctp-blue/30 hover:fill-ctp-blue transition-colors" /> 133 + </a> 134 + </div> 172 135 </div> 136 + <p class="text-ctp-text">{userQuery.data.profile.description}</p> 173 137 </div> 174 - <p class="text-ctp-text">{userQuery.data.profile.description}</p> 175 138 176 139 <Actions 177 140 initialSelectedLabelVals={userQuery.data.repo.labels?.map((label) => label.val) ?? []} 178 - onSubmit={(reason, labels) => submitLabels(reason, labels)} 141 + ref={{ 142 + $type: 'com.atproto.admin.defs#repoRef', 143 + did: actor 144 + }} 145 + onRefresh={async () => { 146 + await userQuery.refetch(); 147 + await eventsQuery.refetch(); 148 + }} 179 149 /> 180 150 {:else if userQuery.isError} 181 151 <p class="text-sm text-ctp-red">
+7 -1
src/lib/types.ts
··· 5 5 reason: string | null; 6 6 }; 7 7 createdAt: string; 8 - } 8 + } 9 + 10 + export type DropdownOption = { 11 + value: string; 12 + label: string; 13 + disabled?: boolean; 14 + };