Skip to content

Commit 153a14d

Browse files
authored
feat: allow chatters to set their own username paint if setting is enabled (#138)
* feat: allow chatters to set their own username paint if setting is enabled * chore: updated changelog * ref: moved button to second position
1 parent b09bc21 commit 153a14d

File tree

5 files changed

+189
-3
lines changed

5 files changed

+189
-3
lines changed

changelog.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const changelog: Changelog[] = [
77
changes: `
88
- added support for Fansly stream titles and uptimes in feed suggestions list
99
- added emote suggestions to chat input
10+
- added "Set Username Paint" button to user actions modal
11+
- chatters can now set their username paint if the creator has enabled it and is a ZerGo0_Bot subscriber
1012
- fixed emotes not showing up in chat when using new lines
1113
`
1214
},

src/lib/api/zergo0.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import type {
33
ZerGo0Badge,
44
ZerGo0Emote,
55
ZerGo0Response,
6-
ZerGo0UsernamePaint
6+
ZerGo0UsernamePaint,
7+
ZerGo0UsernamePaintSettings
78
} from '../types';
89
import { Cache } from '../utils/cache';
910
import { deduplicatedFetch } from '../utils/requestDeduplicator';
@@ -160,6 +161,30 @@ class Zergo0Api {
160161
this.usernamePaintCache.set(username, paintPromise);
161162
return paintPromise;
162163
}
164+
165+
async getUsernamePaintSettings(creatorId: string): Promise<boolean> {
166+
try {
167+
const resp = await deduplicatedFetch(
168+
`https://zergo0_bot.zergo0.dev/ftv/username-paint/usersettings?creatorId=${creatorId}`
169+
);
170+
if (!resp.ok) {
171+
console.warn('Username paint settings request failed', resp);
172+
return false;
173+
}
174+
175+
const json = (await resp.json()) as ZerGo0Response<ZerGo0UsernamePaintSettings>;
176+
if (!json || !json.success) {
177+
console.warn('Could not parse username paint settings response');
178+
return false;
179+
}
180+
181+
// Check if allow_chatters_set_paint is enabled
182+
return json.response.allow_chatters_set_paint === 'true';
183+
} catch (error) {
184+
console.warn('Username paint settings request failed', error);
185+
return false;
186+
}
187+
}
163188
}
164189

165190
export const zergo0Api = new Zergo0Api();

src/lib/components/ui/useractions/ActionsButtonModal.svelte

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang="ts">
2+
import { zergo0Api } from '@/lib/api/zergo0';
23
import { sharedState } from '@/lib/state/state.svelte';
34
import { ActionType } from '@/lib/types';
45
import { Button } from '../button';
@@ -7,6 +8,7 @@
78
import ChangelogModal from './actions/ChangelogModal.svelte';
89
import ChatPollModal from './actions/ChatPollModal.svelte';
910
import GiveawayModal from './actions/GiveawayModal.svelte';
11+
import UsernamePaintModal from './actions/UsernamePaintModal.svelte';
1012
1113
interface Props {
1214
showModal: boolean;
@@ -17,6 +19,16 @@
1719
let actionModal: any;
1820
1921
let action: ActionType = $state(ActionType.None);
22+
let hasUsernamePaintPermission: boolean = $state(false);
23+
24+
// Check for username paint permissions when modal opens
25+
$effect(() => {
26+
if (showModal && sharedState.chatroomId) {
27+
zergo0Api.getUsernamePaintSettings(sharedState.chatroomId).then((hasPermission) => {
28+
hasUsernamePaintPermission = hasPermission;
29+
});
30+
}
31+
});
2032
2133
function handleChangelog() {
2234
action = ActionType.Changelog;
@@ -29,6 +41,10 @@
2941
function handleStartGiveaway() {
3042
action = ActionType.Giveaway;
3143
}
44+
45+
function handleSetUsernamePaint() {
46+
action = ActionType.UsernamePaint;
47+
}
3248
</script>
3349

3450
<Modal bind:showModal bind:this={actionModal}>
@@ -40,9 +56,11 @@
4056
<div class="flex flex-col space-y-2">
4157
<Button variant="secondary" onclick={handleChangelog} class="relative">
4258
<UpdateDot class="-right-1 -top-1" />
43-
4459
Changelog
4560
</Button>
61+
{#if hasUsernamePaintPermission}
62+
<Button variant="secondary" onclick={handleSetUsernamePaint}>Set Username Paint</Button>
63+
{/if}
4664

4765
{#if sharedState.isOwner || sharedState.isModerator}
4866
<Button variant="secondary" onclick={handleStartPoll}>Start Poll</Button>
@@ -59,4 +77,6 @@
5977
<ChatPollModal bind:action />
6078
{:else if action === ActionType.Giveaway}
6179
<GiveawayModal bind:action />
80+
{:else if action === ActionType.UsernamePaint}
81+
<UsernamePaintModal bind:action />
6282
{/if}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<script lang="ts">
2+
import { fanslyApi } from '@/lib/api/fansly.svelte';
3+
import { usernamePaintDesigns } from '@/lib/entryPoints/accountCard';
4+
import { sharedState } from '@/lib/state/state.svelte';
5+
import { ActionType } from '@/lib/types';
6+
import Button from '../../button/button.svelte';
7+
import Label from '../../label/label.svelte';
8+
import Modal from '../../modal/Modal.svelte';
9+
10+
interface Props {
11+
action: ActionType;
12+
}
13+
14+
let { action = $bindable() }: Props = $props();
15+
16+
let selectedPaintId: number = $state(0);
17+
let previewText: string = $state('Preview');
18+
let error: string | null = $state(null);
19+
20+
function onClose() {
21+
action = ActionType.None;
22+
}
23+
24+
async function onSavePaint() {
25+
error = null;
26+
27+
try {
28+
const result = await fanslyApi.sendChatMessage(
29+
sharedState.chatroomId!,
30+
`!setpaint ${selectedPaintId}`
31+
);
32+
33+
if (!result) {
34+
error = 'Failed to set username paint, please try again later.';
35+
return;
36+
}
37+
38+
onClose();
39+
40+
// Reload the page to apply the new username paint
41+
setTimeout(() => {
42+
window.location.reload();
43+
}, 100);
44+
} catch (err) {
45+
error = 'An error occurred while setting username paint.';
46+
console.error('Error setting username paint:', err);
47+
}
48+
}
49+
50+
$effect(() => {
51+
// Load userpaint CSS if not already loaded
52+
const head = document.head;
53+
if (!head.querySelector('style#ftv-userpaint-css')) {
54+
import('@/assets/userpaint.css?inline').then((module) => {
55+
const style = document.createElement('style');
56+
style.id = 'ftv-userpaint-css';
57+
style.media = 'screen';
58+
style.innerHTML = module.default;
59+
document.head.appendChild(style);
60+
});
61+
}
62+
});
63+
64+
const selectedDesign = $derived(
65+
usernamePaintDesigns.find((design) => design.id === selectedPaintId) || usernamePaintDesigns[0]
66+
);
67+
</script>
68+
69+
<Modal showModal={true} {onClose}>
70+
{#snippet header()}
71+
<h2>Set Username Paint</h2>
72+
{/snippet}
73+
74+
{#snippet body()}
75+
<div class="flex flex-col space-y-4">
76+
<div>
77+
<Label for="paint-select">Select Username Paint</Label>
78+
<select
79+
id="paint-select"
80+
class="mt-2 flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
81+
bind:value={selectedPaintId}
82+
>
83+
{#each usernamePaintDesigns as design}
84+
<option value={design.id}>{design.id}</option>
85+
{/each}
86+
</select>
87+
</div>
88+
89+
<div>
90+
<Label>Preview</Label>
91+
<div class="mt-2 rounded-md border border-input bg-background p-4">
92+
<div class="flex items-center justify-center">
93+
<span
94+
class="relative inline-block text-lg font-bold {selectedDesign.class
95+
? 'userpaints-' + selectedDesign.class
96+
: ''} {selectedDesign.textClass ? 'userpaints-' + selectedDesign.textClass : ''}"
97+
>
98+
{previewText}
99+
{#if selectedDesign.gif}
100+
<img
101+
src="https://zergo0botcdn.zergo0.dev/assets/{selectedDesign.gif}.gif"
102+
alt="Username paint effect"
103+
class="userpaints-effect-overlay"
104+
/>
105+
{/if}
106+
</span>
107+
</div>
108+
</div>
109+
</div>
110+
111+
<div>
112+
<Label for="preview-text">Preview Text</Label>
113+
<input
114+
id="preview-text"
115+
type="text"
116+
class="mt-2 flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
117+
bind:value={previewText}
118+
placeholder="Enter preview text"
119+
/>
120+
</div>
121+
122+
<hr />
123+
124+
{#if error}
125+
<p class="text-xs text-red-500">{error}</p>
126+
{/if}
127+
128+
<div class="flex justify-end space-x-2">
129+
<Button variant="secondary" size="sm" class="w-full" onclick={onClose}>Cancel</Button>
130+
<Button variant="default" size="sm" class="w-full" onclick={onSavePaint}>Save Paint</Button>
131+
</div>
132+
</div>
133+
{/snippet}
134+
</Modal>

src/lib/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export enum ActionType {
2828
None = 'none',
2929
Changelog = 'changelog',
3030
ChatPoll = 'chatPoll',
31-
Giveaway = 'giveaway'
31+
Giveaway = 'giveaway',
32+
UsernamePaint = 'usernamePaint'
3233
}
3334

3435
export type FanslyResponse<T> = {
@@ -717,3 +718,7 @@ export interface ZerGo0Badge {
717718
export interface ZerGo0UsernamePaint {
718719
usernamePaintId: number;
719720
}
721+
722+
export interface ZerGo0UsernamePaintSettings {
723+
allow_chatters_set_paint: string;
724+
}

0 commit comments

Comments
 (0)