- Anyone who can see a play can read its comments; the player and the people they follow can write. The author, the player and admins can delete; comments can be reported (admins see the text). - Notifications: "commented on" for the player, "replied on" for earlier commenters, each with its own setting. Reaction and comment notifications now open the play. - A 💬 chip with the count next to reactions on the feed and recent plays, opening the thread on the app (/play/[id]) and the website. Threads refresh every few seconds while open. - Blocking removes comments between the two people. Plays with comments survive history trimming. - Privacy page updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Y2iUsKEWDuRGjK5FPmcUP
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
import { z } from 'zod';
|
|
import { STREAMING_SERVICES } from './services';
|
|
|
|
export const usernameSchema = z
|
|
.string()
|
|
.trim()
|
|
.toLowerCase()
|
|
.regex(/^[a-z0-9_]{3,32}$/, 'Username must be 3-32 characters: letters, numbers, underscores');
|
|
|
|
// Trim before validating: iOS keyboards often append a space after autocompleting an email.
|
|
const emailSchema = z.string().trim().toLowerCase().pipe(z.email());
|
|
|
|
const newPasswordSchema = z.string().min(8, 'Password must be at least 8 characters').max(200);
|
|
|
|
export const registerSchema = z.object({
|
|
username: usernameSchema,
|
|
email: emailSchema,
|
|
password: newPasswordSchema,
|
|
});
|
|
|
|
export const forgotPasswordSchema = z.object({ email: emailSchema });
|
|
|
|
export const verifyEmailSchema = z.object({ token: z.string().min(20).max(200) });
|
|
|
|
export const changePasswordSchema = z.object({
|
|
currentPassword: z.string().min(1).max(200),
|
|
newPassword: newPasswordSchema,
|
|
});
|
|
|
|
export const changeEmailSchema = z.object({
|
|
email: emailSchema,
|
|
password: z.string().min(1).max(200),
|
|
});
|
|
|
|
export const resetPasswordSchema = z.object({
|
|
token: z.string().min(20).max(200),
|
|
password: newPasswordSchema,
|
|
});
|
|
|
|
export const loginSchema = z.object({
|
|
email: emailSchema,
|
|
password: z.string().min(1).max(200),
|
|
});
|
|
|
|
export const updateMeSchema = z.object({
|
|
displayName: z.string().trim().max(64).nullable().optional(),
|
|
bio: z.string().trim().max(280).nullable().optional(),
|
|
preferredService: z.enum(STREAMING_SERVICES).nullable().optional(),
|
|
services: z
|
|
.array(z.enum(STREAMING_SERVICES))
|
|
.max(STREAMING_SERVICES.length)
|
|
.transform((s) => [...new Set(s)])
|
|
.nullable()
|
|
.optional(),
|
|
notifyPrefs: z
|
|
.object({ reaction: z.boolean(), comment: z.boolean(), reply: z.boolean(), follow: z.boolean(), share: z.boolean() })
|
|
.partial()
|
|
.optional(),
|
|
/** Mark onboarding as finished. */
|
|
onboarded: z.literal(true).optional(),
|
|
});
|
|
|
|
/** Send a song to people who follow you. */
|
|
export const sendSongSchema = z.object({
|
|
trackId: z.uuid(),
|
|
to: z.array(z.string().min(1).max(32)).min(1).max(20),
|
|
message: z.string().trim().max(280).nullable().optional(),
|
|
});
|
|
|
|
/** A browser's or phone's push subscription (both use Web Push encryption keys). */
|
|
export const pushSubscriptionSchema = z.object({
|
|
kind: z.enum(['web', 'unifiedpush']),
|
|
endpoint: z.url({ protocol: /^https?$/ }).max(2000),
|
|
keys: z.object({ p256dh: z.string().min(20).max(200), auth: z.string().min(8).max(100) }),
|
|
});
|
|
|
|
export const setFeaturedSchema = z.union([
|
|
z.object({ deezerId: z.string().regex(/^\d+$/), comment: z.string().trim().max(280).nullable().optional() }),
|
|
z.object({ trackId: z.uuid(), comment: z.string().trim().max(280).nullable().optional() }),
|
|
]);
|
|
|
|
export const connectUsernameSchema = z.object({
|
|
username: z.string().trim().min(1).max(64),
|
|
});
|
|
|
|
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
export type LoginInput = z.infer<typeof loginSchema>;
|
|
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
|
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
|
|
export type ChangeEmailInput = z.infer<typeof changeEmailSchema>;
|
|
export type UpdateMeInput = z.infer<typeof updateMeSchema>;
|
|
export type SetFeaturedInput = z.infer<typeof setFeaturedSchema>;
|
|
export type SendSongInput = z.infer<typeof sendSongSchema>;
|
|
export type PushSubscriptionInput = z.infer<typeof pushSubscriptionSchema>;
|
|
|
|
export const jellyfinConnectSchema = z.object({
|
|
serverUrl: z.string().trim().min(1).max(500),
|
|
username: z.string().trim().min(1).max(128),
|
|
password: z.string().max(256).default(''),
|
|
});
|
|
|
|
/** Pause sharing: for an hour, until tomorrow, until turned back on, or resume (null). */
|
|
export const PAUSE_OPTIONS = ['1h', '8h', 'indefinitely'] as const;
|
|
export const setSharingSchema = z.object({ pause: z.enum(PAUSE_OPTIONS).nullable() });
|
|
|
|
export const REPORT_REASONS = ['spam', 'harassment', 'inappropriate', 'impersonation', 'other'] as const;
|
|
export type ReportReason = (typeof REPORT_REASONS)[number];
|
|
export const REPORT_REASON_LABELS: Record<ReportReason, string> = {
|
|
spam: 'Spam or fake account',
|
|
harassment: 'Harassment or bullying',
|
|
inappropriate: 'Inappropriate profile or picture',
|
|
impersonation: 'Pretending to be someone else',
|
|
other: 'Something else',
|
|
};
|
|
export const COMMENT_MAX_LENGTH = 500;
|
|
export const commentSchema = z.object({
|
|
body: z.string().trim().min(1, 'Write something first').max(COMMENT_MAX_LENGTH, `Keep it under ${COMMENT_MAX_LENGTH} characters`),
|
|
});
|
|
export type CommentInput = z.infer<typeof commentSchema>;
|
|
|
|
export const reportSchema = z.object({
|
|
reason: z.enum(REPORT_REASONS),
|
|
note: z.string().trim().max(500).nullish(),
|
|
});
|