The weeb for the next gen discord boat - Wamellow wamellow.com
bot discord
3
fork

Configure Feed

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

refactor notification styles

Luna b10fe7a9 45f7c928

+81 -60
+8 -5
app/dashboard/[guildId]/notifications/page.tsx
··· 43 43 items, 44 44 setItemId, 45 45 editItem, 46 + editObj, 46 47 addItem, 47 48 removeItem, 48 49 isLoading, ··· 169 170 name="Channel" 170 171 url={url + "/" + item.id} 171 172 dataName="channelId" 172 - items={createSelectableItems(guild?.channels)} 173 + items={createSelectableItems(guild?.channels, ["ViewChannel", "SendMessages", "EmbedLinks", item.username ? "ManageWebhooks" : null, item.roleId ? "MentionEveryone" : null])} 173 174 description="Select a channel where notifications should be send into." 174 175 defaultState={item.channelId} 175 176 onSave={(o) => editItem("channelId", o.value as string)} ··· 228 229 dataName="regex" 229 230 description="Posts that match the provided regex will be ignored." 230 231 defaultState={item.regex || ""} 232 + onSave={(value) => editItem("regex", value as string)} 231 233 /> 232 234 } 233 235 </div> ··· 240 242 dataName="regex" 241 243 description="Posts that match the provided regex will be ignored." 242 244 defaultState={item.regex || ""} 245 + onSave={(value) => editItem("regex", value as string)} 243 246 /> 244 247 )} 245 248 246 249 <NotificationStyle 247 250 item={item} 248 251 premium={premium} 249 - onEdit={(style) => editItem("style", style)} 252 + onEdit={editObj} 250 253 /> 251 254 252 255 <MessageCreatorEmbed ··· 255 258 url={url + "/" + item.id} 256 259 dataName="message" 257 260 defaultMessage={item.message} 258 - user={premium && item.style 261 + user={premium && item.username 259 262 ? { 260 - username: item.style.name || "", 261 - avatar: item.style.avatarUrl || "/discord.webp", 263 + username: item.username || "", 264 + avatar: item.avatar ? `https://r2.wamellow.com/avatars/webhooks/${item.avatar}` : "/discord.webp", 262 265 bot: true 263 266 } 264 267 : undefined
+51 -49
app/dashboard/[guildId]/notifications/style.component.tsx
··· 1 1 "use client"; 2 2 import Link from "next/link"; 3 - import { useRef, useState } from "react"; 3 + import { useMemo, useRef, useState } from "react"; 4 4 import { HiOutlineUpload, HiPencil, HiSparkles, HiX } from "react-icons/hi"; 5 5 6 6 import { guildStore } from "@/common/guilds"; ··· 10 10 import { Section } from "@/components/section"; 11 11 import { UserAvatar } from "@/components/ui/avatar"; 12 12 import { Button } from "@/components/ui/button"; 13 - import type { ApiV1GuildsModulesNotificationsGetResponse } from "@/typings"; 13 + import type { ApiV1GuildsModulesNotificationsGetResponse, ApiV1GuildsModulesNotificationStylePatchResponse } from "@/typings"; 14 14 import { cn } from "@/utils/cn"; 15 15 16 16 const ALLOWED_FILE_TYPES = ["image/png", "image/jpeg", "image/webp"]; ··· 23 23 }: { 24 24 item: ApiV1GuildsModulesNotificationsGetResponse; 25 25 premium: boolean; 26 - onEdit: (opts: { name: string; avatarUrl: string | null; }) => void; 26 + onEdit: (opts: ApiV1GuildsModulesNotificationStylePatchResponse) => void; 27 27 }) { 28 28 const [open, setOpen] = useState(false); 29 29 ··· 33 33 34 34 <div className="backdrop-blur-3xl backdrop-brightness-[25%] rounded-[6px] pr-4 py-4 pl-6 md:py-8 md:pl-10 flex gap-6 items-center"> 35 35 <UserAvatar 36 - alt={premium && item.style ? (item.style.name || "") : "Wamellow"} 36 + alt={premium && item.username ? item.username : "Wamellow"} 37 37 className="size-24" 38 - src={premium && item.style ? (item.style.avatarUrl || "/discord.webp") : "/waya-v3.webp"} 38 + src={premium && item.username && item.avatar ? `https://r2.wamellow.com/avatars/webhooks/${item.avatar}` : "/waya-v3.webp"} 39 39 /> 40 40 41 41 <div className="space-y-2"> 42 42 <span className="text-3xl font-medium text-primary-foreground"> 43 - {premium && item.style ? (item.style.name || "Unknown") : "Wamellow"} 43 + {premium && item.username ? item.username : "Wamellow"} 44 44 </span> 45 - {premium 46 - ? <Button onClick={() => setOpen(true)}> 47 - <HiPencil /> 48 - Change Style 49 - </Button> 50 - : <Button asChild> 51 - <Link 52 - href={`/premium?utm_source=${window.location.hostname}&utm_medium=notification-styles`} 53 - target="_blank" 54 - > 55 - <HiSparkles /> 45 + <div className="flex"> 46 + {premium 47 + ? <Button onClick={() => setOpen(true)}> 48 + <HiPencil /> 56 49 Change Style 57 - </Link> 58 - </Button> 59 - } 50 + </Button> 51 + : <Button asChild> 52 + <Link 53 + href={`/premium?utm_source=${window.location.hostname}&utm_medium=notification-styles`} 54 + target="_blank" 55 + > 56 + <HiSparkles /> 57 + Change Style 58 + </Link> 59 + </Button> 60 + } 61 + </div> 60 62 </div> 61 63 62 64 <ExampleMessages /> ··· 65 67 66 68 <ChangeStyleModal 67 69 id={item.id} 68 - username={item.style?.name || null} 69 - avatarUrl={item.style?.avatarUrl || null} 70 + username={item.username || null} 71 + avatarUrl={item.avatar ? `https://r2.wamellow.com/avatars/webhooks/${item.avatar}` : null} 70 72 71 73 isOpen={open} 72 74 onClose={() => setOpen(false)} ··· 116 118 117 119 isOpen: boolean; 118 120 onClose: () => void; 119 - onEdit: (opts: { name: string; avatarUrl: string | null; }) => void; 121 + onEdit: (opts: ApiV1GuildsModulesNotificationStylePatchResponse) => void; 120 122 } 121 123 122 124 export function ChangeStyleModal({ ··· 132 134 const avatarRef = useRef<HTMLInputElement | null>(null); 133 135 134 136 const [name, setName] = useState(username); 135 - const [avatar, setAvatar] = useState<string | null>(avatarUrl); 137 + const [avatar, setAvatar] = useState<ArrayBuffer | string | null>(avatarUrl); 136 138 const [error, setError] = useState<string | null>(null); 137 139 140 + const renderable = useMemo( 141 + () => !avatar || typeof avatar === "string" 142 + ? avatar || "/waya-v3.webp" 143 + : URL.createObjectURL(new Blob([avatar])), 144 + [avatar] 145 + ); 146 + 138 147 return (<> 139 - <Modal<ApiV1GuildsModulesNotificationsGetResponse> 148 + <Modal<ApiV1GuildsModulesNotificationStylePatchResponse> 140 149 title="Edit Notification Style" 141 150 isOpen={isOpen} 142 - onClose={onClose} 151 + onClose={() => { 152 + onClose(); 153 + setError(null); 154 + }} 143 155 onSubmit={() => { 144 156 const valid = isValidUsername(name); 145 - if (!valid) return new Error("Invalid name"); 157 + if (!name || !valid) return new Error("Invalid name"); 158 + 159 + const formData = new FormData(); 160 + formData.append("json_payload", JSON.stringify({ username: name })); 161 + if (avatar && typeof avatar !== "string") formData.append("file", new Blob([avatar])); 146 162 147 163 return fetch(`${process.env.NEXT_PUBLIC_API}/guilds/${guildId}/modules/notifications/${id}/style`, { 148 164 method: "PATCH", 149 165 credentials: "include", 150 - headers: { 151 - "Content-Type": "application/json" 152 - }, 153 - body: JSON.stringify({ 154 - name, 155 - avatar: avatar?.startsWith("data:") ? avatar : null 156 - }) 166 + body: formData 157 167 }); 158 168 }} 159 - onSuccess={() => { 160 - onEdit({ name: name!, avatarUrl: avatar! }); 169 + onSuccess={(style) => { 170 + onEdit(style); 171 + setError(null); 161 172 }} 162 173 isDisabled={!name || Boolean(error)} 163 174 > ··· 187 198 return; 188 199 } 189 200 190 - const reader = new FileReader(); 191 201 192 - reader.onload = () => { 193 - if (typeof reader.result === "string") { 194 - setAvatar(reader.result); 195 - } 196 - }; 197 - 198 - reader.onerror = () => { 199 - setError("Failed to read the file."); 200 - }; 201 - 202 - reader.readAsDataURL(file); 202 + file.arrayBuffer().then((buffer) => { 203 + setAvatar(buffer); 204 + }); 203 205 }} 204 206 ref={avatarRef} 205 207 type="file" ··· 229 231 mode="DARK" 230 232 user={{ 231 233 username: name || "Wamellow", 232 - avatar: avatar || "/waya-v3.webp", 234 + avatar: renderable, 233 235 bot: true 234 236 }} 235 237 >
+13
components/dashboard/lists/hook.ts
··· 55 55 [item, data, url, queryClient] 56 56 ); 57 57 58 + const editObj = useCallback( 59 + <K extends keyof T>(payload: Record<K, T[K]>) => { 60 + if (!item || !Array.isArray(data)) return; 61 + 62 + queryClient.setQueryData<T[]>(url, () => [ 63 + ...(data?.filter((t) => t.id !== item.id) || []), 64 + { ...item, ...payload } 65 + ]); 66 + }, 67 + [item, data, url, queryClient] 68 + ); 69 + 58 70 const addItem = useCallback( 59 71 (newItem: T) => { 60 72 if (!Array.isArray(data)) return; ··· 83 95 items: Array.isArray(data) ? data : [], 84 96 setItemId, 85 97 editItem, 98 + editObj, 86 99 addItem, 87 100 removeItem, 88 101 isLoading,
+7 -4
typings.ts
··· 466 466 avatarUrl: string | null; 467 467 }; 468 468 469 - style: { 470 - name: string | null; 471 - avatarUrl: string | null; 472 - } | null 469 + username: string | null; 470 + avatar: string | null; 471 + } 472 + 473 + export interface ApiV1GuildsModulesNotificationStylePatchResponse { 474 + username: string; 475 + avatar: string | null; 473 476 } 474 477 475 478 export enum DailypostType {
+2 -2
utils/create-selectable-items.tsx
··· 18 18 19 19 export function createSelectableItems<T extends Item>( 20 20 items: T[] | undefined, 21 - requiredPermissions?: PermissionNames[], 21 + requiredPermissions?: (PermissionNames | null)[], 22 22 allowedTypes: ChannelType[] = [ChannelType.GuildText, ChannelType.GuildAnnouncement] 23 23 ) { 24 24 if (!items?.length) return []; ··· 36 36 value: item.id, 37 37 color: "color" in item ? item.color : undefined, 38 38 error: "permissions" in item 39 - ? parsePermissions(item.permissions, requiredPermissions || []).join(", ") 39 + ? parsePermissions(item.permissions, requiredPermissions?.filter((perm) => perm !== null) || []).join(", ") 40 40 : undefined 41 41 })); 42 42 }