Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/packages/settings/src/components/cardSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function CardSettings() {
return !info?.permission_catalogue?.includes("upload_banner_image") && permissions.includes("upload_avatar_image");
};
const bannerHosts = connected.filter(mayUpload);
const videoBannerHosts = bannerHosts.filter((h) => serverDetailsList[h]?.server_info?.video_profiles === true);
const [bannerPreview, setBannerPreview] = useState<string | null>();
const [pendingBanner, setPendingBanner] = useState<File | null>();
const [editing, setEditing] = useState(false);
Expand Down Expand Up @@ -195,7 +196,10 @@ export function CardSettings() {
setBannerBusy(true);
const results = pendingBanner === undefined
? []
: await Promise.allSettled(bannerHosts.map((h) => sendBanner(h, pendingBanner)));
// A video only goes where it is taken; the other servers keep the banner they have.
: await Promise.allSettled(
(pendingBanner?.type.startsWith("video/") ? videoBannerHosts : bannerHosts).map((h) => sendBanner(h, pendingBanner)),
);
setBannerBusy(false);
const failed = results.filter((r): r is PromiseRejectedResult => r.status === "rejected");
if (results.length > 0 && failed.length === results.length) {
Expand Down Expand Up @@ -277,6 +281,13 @@ export function CardSettings() {
: `A picture across the top of your card, on the ${bannerHosts.length} of your servers that let you upload. The others show your pattern.`
}
>
<span className="text-xs text-gryt-muted">
{videoBannerHosts.length === 0
? "Pictures only: none of your servers takes a video banner."
: videoBannerHosts.length < bannerHosts.length
? `A video banner goes to the ${videoBannerHosts.length} of your servers that take one. The others keep the banner they have.`
: "A picture or a short video. A video plays silently, up to ten seconds."}
</span>
<div className="flex flex-wrap gap-2">
<Button size="small" disabled={bannerBusy} onClick={() => bannerInput.current?.click()}>
{bannerUrl ? "Change banner" : "Upload a banner"}
Expand All @@ -290,7 +301,7 @@ export function CardSettings() {
<input
ref={bannerInput}
type="file"
accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm"
accept={videoBannerHosts.length > 0 ? "image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm" : "image/png,image/jpeg,image/webp,image/gif"}
style={{ display: "none" }}
onChange={(e) => {
const file = e.target.files?.[0];
Expand Down
17 changes: 16 additions & 1 deletion src/packages/settings/src/components/profileSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,19 @@ export function ProfileSettings() {

hosts = permitted;

// A video only goes to servers that take one; the rest keep the avatar they have.
if (isVideo) {
const takesVideo = hosts.filter((h) => serverDetailsList?.[h]?.server_info?.video_profiles === true);
if (takesVideo.length === 0) {
toast.error(hosts.length === 1 ? "This server doesn't take video avatars. Pick a picture instead." : "None of these servers take video avatars. Pick a picture instead.");
return;
}
if (takesVideo.length < hosts.length) {
toast(`Your video goes to the ${takesVideo.length} of ${hosts.length} servers that take one. The others keep your current avatar.`);
}
hosts = takesVideo;
}

setUploading(true);
try {
if (hosts.length === 0) {
Expand Down Expand Up @@ -959,7 +972,9 @@ export function ProfileSettings() {
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm,video/quicktime"
accept={serverHosts.some((h) => serverDetailsList?.[h]?.server_info?.video_profiles === true)
? "image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm,video/quicktime"
: "image/png,image/jpeg,image/webp,image/gif"}
style={{ display: "none" }}
onChange={handleFileChange}
/>
Expand Down
4 changes: 4 additions & 0 deletions src/packages/settings/src/types/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export type serverDetails = {
voice_enabled?: boolean;
avatar_max_bytes?: number | null;
upload_max_bytes?: number | null;
/** Uploads are checked in the image worker's sandbox first. Absent on an older server. */
uploads_checked?: boolean;
/** Video avatars and banners are taken here: the host can and the owner allows it. */
video_profiles?: boolean;
version?: string;
/**
* Every plugin this server is running, and what each may do. All of them — an
Expand Down
37 changes: 37 additions & 0 deletions src/packages/socket/src/components/ServerOverviewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type ServerSettingsPayload = {
discoverable?: boolean;
spamFilter?: boolean;
spamSensitivity?: SpamSensitivity;
videoProfiles?: boolean;
/** What this host can do with uploads; an older server sends nothing. */
media?: { checked?: boolean; video?: boolean };
};

export type ServerOverviewInitialSettings = {
Expand Down Expand Up @@ -102,6 +105,8 @@ export function ServerOverviewTab({
const [discoverable, setDiscoverable] = useState(true);
const [spamFilter, setSpamFilter] = useState(true);
const [spamSensitivity, setSpamSensitivity] = useState<SpamSensitivity>("normal");
const [videoProfiles, setVideoProfiles] = useState(true);
const [media, setMedia] = useState<{ checked: boolean; video: boolean } | null>(null);

const [autosaving, setAutosaving] = useState(false);
const pendingSaveCountRef = useRef(0);
Expand All @@ -119,6 +124,7 @@ export function ServerOverviewTab({
discoverable: boolean;
spamFilter: boolean;
spamSensitivity: SpamSensitivity;
videoProfiles: boolean;
} | null>(null);

const [avatarMaxMb, setAvatarMaxMb] = useState<string>("");
Expand Down Expand Up @@ -176,6 +182,8 @@ export function ServerOverviewTab({
setDiscoverable(payload.discoverable !== false);
setSpamFilter(payload.spamFilter !== false);
setSpamSensitivity(normalizeSpamSensitivity(payload.spamSensitivity));
setVideoProfiles(payload.videoProfiles !== false);
setMedia(payload.media ? { checked: !!payload.media.checked, video: !!payload.media.video } : null);

if (!wasSaving) {
setDisplayName(payload.displayName || "");
Expand Down Expand Up @@ -204,6 +212,7 @@ export function ServerOverviewTab({
discoverable: payload.discoverable !== false,
spamFilter: payload.spamFilter !== false,
spamSensitivity: normalizeSpamSensitivity(payload.spamSensitivity),
videoProfiles: payload.videoProfiles !== false,
};
};

Expand Down Expand Up @@ -265,6 +274,7 @@ export function ServerOverviewTab({
discoverable: boolean;
spamFilter: boolean;
spamSensitivity: SpamSensitivity;
videoProfiles: boolean;
}>): boolean => {
if (!host || !socket || !socket.connected) {
toast.error("Not connected to the server.");
Expand Down Expand Up @@ -557,6 +567,33 @@ export function ServerOverviewTab({
/>
</div>

{media && (
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Uploads</span>
<span className="text-xs" style={{ lineHeight: 1.4 }}>
{media.checked
? "New pictures and videos are checked and written out again in a sandbox before anyone sees them."
: "This server can't check uploads in a sandbox, so it shrinks pictures itself and can't take videos. Hosting from the Gryt desktop app, or with Docker, does both."}
</span>
<div className="flex items-center gap-2">
<Switch
checked={media.video && videoProfiles}
onCheckedChange={(v) => {
setVideoProfiles(v);
if (!saveIfChanged({ videoProfiles: v })) setVideoProfiles(!v);
}}
disabled={!canEdit || !media.video}
/>
<span className="text-sm">Let members use video avatars and banners</span>
</div>
<span className="text-xs text-gryt-muted" style={{ lineHeight: 1.4 }}>
{media.video
? "Each video becomes a silent clip of up to ten seconds, at the size of the card."
: "Not available here: it needs the same sandbox."}
</span>
</div>
)}

<div className="flex flex-col gap-2">
<span className="text-sm font-medium">
Limits (optional)
Expand Down
Loading