Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 2 additions & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@mediapipe/tasks-vision": "^0.10.35",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
Expand Down Expand Up @@ -70,6 +71,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tiptap-markdown": "^0.9.0",
"upng-js": "^2.1.0",
"yaml": "^2.8.3",
"zod": "^4.4.3"
},
Expand Down
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default defineConfig({
"**/identity-archive.spec.ts",
"**/identity-archive-hide.spec.ts",
"**/relay-connectivity-screenshots.spec.ts",
"**/animated-avatar-screenshots.spec.ts",
],
use: {
...devices["Desktop Chrome"],
Expand Down
2 changes: 2 additions & 0 deletions desktop/src-tauri/Entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
2 changes: 2 additions & 0 deletions desktop/src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<string>Buzz</string>
<key>NSMicrophoneUsageDescription</key>
<string>Buzz needs microphone access for voice huddles.</string>
<key>NSCameraUsageDescription</key>
<string>Buzz needs camera access to record animated avatars.</string>
</dict>
</plist>
88 changes: 79 additions & 9 deletions desktop/src/features/onboarding/ui/AvatarStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,58 @@ export function AvatarStep({ actions, direction, state }: AvatarStepProps) {
const [avatarSquishKey, setAvatarSquishKey] = React.useState(0);
const [avatarEditorMode, setAvatarEditorMode] =
React.useState<AvatarMode>("image");
const [animatedPreviewEl, setAnimatedPreviewEl] =
React.useState<HTMLDivElement | null>(null);
const [isAnimatedPreviewActive, setIsAnimatedPreviewActive] =
React.useState(false);
const [animatedPreviewCaption, setAnimatedPreviewCaption] = React.useState<
string | null
>(null);
const [pendingAnimatedAvatarUrl, setPendingAnimatedAvatarUrl] =
React.useState<string | null>(null);
const [isCustomColorPickerOpen, setIsCustomColorPickerOpen] =
React.useState(false);
const canSubmit =
avatar.draftUrl.trim().length > 0 && !isSaving && !isUploadingAvatar;
const hasAvatarDraft = avatar.draftUrl.trim().length > 0;
const canSubmit = hasAvatarDraft && !isSaving && !isUploadingAvatar;
Comment thread
klopez4212 marked this conversation as resolved.
const isAutoAdvancingAnimatedAvatar = pendingAnimatedAvatarUrl !== null;
const shouldHideActionsForAnimatedAvatar =
avatarEditorMode === "animated" &&
(!hasAvatarDraft || isAutoAdvancingAnimatedAvatar);
Comment thread
klopez4212 marked this conversation as resolved.
const areActionsHidden =
isCustomColorPickerOpen || shouldHideActionsForAnimatedAvatar;
const previewName =
name.draftValue.trim() || name.savedValue.trim() || "Your avatar";
const animateEmojiAvatarChange = React.useCallback(() => {
setAvatarSquishKey((key) => key + 1);
}, []);
const handleAnimatedAvatarApply = React.useCallback((url: string) => {
setPendingAnimatedAvatarUrl(url);
}, []);

React.useEffect(() => {
if (!pendingAnimatedAvatarUrl) {
return;
}
if (avatar.draftUrl !== pendingAnimatedAvatarUrl) {
return;
}
if (saveRecovery.errorMessage) {
setPendingAnimatedAvatarUrl(null);
return;
}
if (isSaving || isUploadingAvatar) {
return;
}

submit();
}, [
avatar.draftUrl,
isSaving,
isUploadingAvatar,
pendingAnimatedAvatarUrl,
saveRecovery.errorMessage,
submit,
]);

return (
<OnboardingSlideTransition
Expand All @@ -270,12 +313,35 @@ export function AvatarStep({ actions, direction, state }: AvatarStepProps) {
</p>
</div>

<div className="mt-12">
<AvatarPreview
avatarSquishKey={avatarSquishKey}
avatarUrl={avatar.draftUrl}
previewName={previewName}
/>
<div className="mt-12 grid justify-items-center gap-3 lg:justify-items-start">
<div className="relative h-48 w-48">
<div
className="pointer-events-none absolute inset-0 z-10"
data-testid="onboarding-avatar-animated-preview-slot"
ref={setAnimatedPreviewEl}
/>
{isAnimatedPreviewActive ? null : (
<AvatarPreview
avatarSquishKey={avatarSquishKey}
avatarUrl={avatar.draftUrl}
previewName={previewName}
/>
)}
</div>

<AnimatePresence initial={false}>
{animatedPreviewCaption ? (
<motion.p
animate={{ opacity: 1, y: 0 }}
className="w-48 text-center text-sm font-medium text-muted-foreground"
exit={{ opacity: 0, y: -4 }}
initial={{ opacity: 0, y: 4 }}
transition={AVATAR_ACTIONS_MOTION_TRANSITION}
>
{animatedPreviewCaption}
</motion.p>
) : null}
</AnimatePresence>
</div>
</div>

Expand All @@ -286,10 +352,14 @@ export function AvatarStep({ actions, direction, state }: AvatarStepProps) {
transition={AVATAR_POSITION_MOTION_TRANSITION}
>
<ProfileAvatarEditor
animatedPreviewContainer={animatedPreviewEl}
avatarUrl={avatar.draftUrl}
disabled={isSaving}
emojiPickerTheme="auto"
emojiPickerThemeVars={NEUTRAL_EMOJI_PICKER_THEME_VARS}
onAnimatedAvatarApply={handleAnimatedAvatarApply}
onAnimatedPreviewActiveChange={setIsAnimatedPreviewActive}
onAnimatedPreviewCaptionChange={setAnimatedPreviewCaption}
onCustomColorPickerOpenChange={setIsCustomColorPickerOpen}
onEmojiAvatarChange={animateEmojiAvatarChange}
onModeChange={setAvatarEditorMode}
Expand All @@ -305,7 +375,7 @@ export function AvatarStep({ actions, direction, state }: AvatarStepProps) {

<AvatarStepActions
canSubmit={canSubmit}
hidden={isCustomColorPickerOpen}
hidden={areActionsHidden}
isSaving={isSaving}
isUploadingAvatar={isUploadingAvatar}
onBack={back}
Expand Down
46 changes: 46 additions & 0 deletions desktop/src/features/profile/lib/animatedAvatarCapture.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import test from "node:test";

import { listAvatarCameras } from "./animatedAvatarCapture.ts";

function mockMediaDevices(devices) {
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
mediaDevices: {
enumerateDevices: async () => devices,
},
},
});
}

test("listAvatarCameras: preserves blank pre-permission camera labels", async () => {
mockMediaDevices([
{
deviceId: "default",
kind: "videoinput",
label: "",
},
{
deviceId: "continuity",
kind: "videoinput",
label: "",
},
{
deviceId: "microphone",
kind: "audioinput",
label: "Studio Mic",
},
]);

const cameras = await listAvatarCameras();

assert.deepEqual(cameras, [
{ deviceId: "default", label: "" },
{ deviceId: "continuity", label: "" },
]);
assert.equal(
cameras.some((device) => device.label.trim().length > 0),
false,
);
});
Loading
Loading