Skip to content

Commit 3b0eb5b

Browse files
Enforce recording limit (#964)
* stop recording after timeout * countdown on recording overlay * share link param + tso text + fix upgrade flow focus * only show the warning for instant mode * only enforce limit on instant mode recording * adding recording limit text into old flow * cleanup message on old recording flow * limit banner in share page + adjustments for colors * remove query param * cleanup * fix * on new recording flow: disable style/early return for buttons while recording --------- Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
1 parent 2c20faa commit 3b0eb5b

File tree

17 files changed

+227
-69
lines changed

17 files changed

+227
-69
lines changed

apps/desktop/src-tauri/src/lib.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2217,12 +2217,26 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
22172217
}
22182218
}
22192219
#[cfg(target_os = "macos")]
2220-
WindowEvent::Focused(focused) if *focused => {
2221-
if let Ok(window_id) = CapWindowId::from_str(label)
2222-
&& window_id.activates_dock()
2223-
{
2224-
app.set_activation_policy(tauri::ActivationPolicy::Regular)
2225-
.ok();
2220+
WindowEvent::Focused(focused) => {
2221+
let window_id = CapWindowId::from_str(label);
2222+
2223+
if matches!(window_id, Ok(CapWindowId::Upgrade)) {
2224+
for (label, window) in app.webview_windows() {
2225+
if let Ok(id) = CapWindowId::from_str(&label)
2226+
&& matches!(id, CapWindowId::TargetSelectOverlay { .. })
2227+
{
2228+
let _ = window.hide();
2229+
}
2230+
}
2231+
}
2232+
2233+
if *focused {
2234+
if let Ok(window_id) = window_id
2235+
&& window_id.activates_dock()
2236+
{
2237+
app.set_activation_policy(tauri::ActivationPolicy::Regular)
2238+
.ok();
2239+
}
22262240
}
22272241
}
22282242
WindowEvent::DragDrop(tauri::DragDropEvent::Drop { paths, .. }) => {

apps/desktop/src/routes/(window-chrome)/(main).tsx

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -486,36 +486,68 @@ function Page() {
486486
Instant Mode
487487
</SignInButton>
488488
) : (
489-
<Button
490-
disabled={toggleRecording.isPending}
491-
variant="blue"
492-
size="md"
493-
onClick={() => toggleRecording.mutate()}
494-
class="flex flex-grow justify-center items-center"
495-
>
496-
{isRecording() ? (
497-
"Stop Recording"
498-
) : (
489+
<Tooltip
490+
childClass="w-full flex"
491+
placement="top"
492+
content={
499493
<>
500-
{rawOptions.mode === "instant" ? (
501-
<IconCapInstant
502-
class={cx(
503-
"size-[0.8rem] mr-1.5",
504-
toggleRecording.isPending ? "opacity-50" : "opacity-100",
505-
)}
506-
/>
507-
) : (
508-
<IconCapFilmCut
509-
class={cx(
510-
"size-[0.8rem] mr-2 -mt-[1.5px]",
511-
toggleRecording.isPending ? "opacity-50" : "opacity-100",
512-
)}
513-
/>
514-
)}
515-
Start Recording
494+
Instant Mode recordings are limited
495+
<br /> to 5 mins,{" "}
496+
<button
497+
class="underline"
498+
onClick={() => commands.showWindow("Upgrade")}
499+
>
500+
Upgrade to Pro
501+
</button>
516502
</>
517-
)}
518-
</Button>
503+
}
504+
openDelay={0}
505+
closeDelay={0}
506+
disabled={
507+
!(
508+
rawOptions.mode === "instant" &&
509+
auth.data?.plan?.upgraded === false
510+
)
511+
}
512+
>
513+
<Button
514+
disabled={toggleRecording.isPending}
515+
variant="blue"
516+
size="md"
517+
onClick={() => toggleRecording.mutate()}
518+
class="flex flex-grow justify-center items-center"
519+
>
520+
{isRecording() ? (
521+
"Stop Recording"
522+
) : (
523+
<>
524+
{rawOptions.mode === "instant" ? (
525+
<IconCapInstant
526+
class={cx(
527+
"size-[0.8rem] mr-1.5",
528+
toggleRecording.isPending
529+
? "opacity-50"
530+
: "opacity-100",
531+
)}
532+
/>
533+
) : (
534+
<IconCapFilmCut
535+
class={cx(
536+
"size-[0.8rem] mr-2 -mt-[1.5px]",
537+
toggleRecording.isPending
538+
? "opacity-50"
539+
: "opacity-100",
540+
)}
541+
/>
542+
)}
543+
{rawOptions.mode === "instant" &&
544+
auth.data?.plan?.upgraded === false
545+
? "Start 5 min recording"
546+
: "Start recording"}
547+
</>
548+
)}
549+
</Button>
550+
</Tooltip>
519551
)}
520552
</div>
521553
</div>

apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function CameraSelect(props: {
3838
<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
3939
<button
4040
disabled={!!currentRecording.data || props.disabled}
41-
class="cursor-default flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors bg-gray-3 disabled:text-gray-11 KSelect"
41+
class="flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors cursor-default disabled:opacity-70 bg-gray-3 disabled:text-gray-11 KSelect"
4242
onClick={() => {
4343
Promise.all([
4444
CheckMenuItem.new({

apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default function MicrophoneSelect(props: {
6666
<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
6767
<button
6868
disabled={!!currentRecording.data || props.disabled}
69-
class="z-10 relative cursor-default flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg overflow-hidden transition-colors bg-gray-3 disabled:text-gray-11 KSelect"
69+
class="flex overflow-hidden relative z-10 flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors cursor-default disabled:opacity-70 bg-gray-3 disabled:text-gray-11 KSelect"
7070
onClick={() => {
7171
Promise.all([
7272
CheckMenuItem.new({

apps/desktop/src/routes/(window-chrome)/new-main/SystemAudio.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function SystemAudio() {
1313
setOptions({ captureSystemAudio: !rawOptions.captureSystemAudio });
1414
}}
1515
disabled={!!currentRecording.data}
16-
class="curosr-default flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors bg-gray-3 disabled:text-gray-11 KSelect"
16+
class="flex flex-row gap-2 items-center px-2 w-full h-9 rounded-lg transition-colors curosr-default disabled:opacity-70 bg-gray-3 disabled:text-gray-11 KSelect"
1717
>
1818
<IconPhMonitorBold class="text-gray-10 size-4" />
1919
<p class="flex-1 text-sm text-left truncate">

apps/desktop/src/routes/(window-chrome)/new-main/TargetTypeButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ function TargetTypeButton(
66
selected: boolean;
77
Component: Component<ComponentProps<"svg">>;
88
name: string;
9+
disabled?: boolean;
910
} & ComponentProps<"div">,
1011
) {
1112
return (
@@ -16,6 +17,7 @@ function TargetTypeButton(
1617
props.selected
1718
? "bg-gray-3 text-white ring-blue-9 ring-1"
1819
: "ring-transparent ring-0",
20+
props.disabled ? "opacity-70 pointer-events-none" : "",
1921
)}
2022
>
2123
<props.Component

apps/desktop/src/routes/(window-chrome)/new-main/index.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { generalSettingsStore } from "~/store";
3030
import { createSignInMutation } from "~/utils/auth";
3131
import {
3232
createCameraMutation,
33+
createCurrentRecordingQuery,
3334
createLicenseQuery,
3435
listAudioDevices,
3536
listScreens,
@@ -112,6 +113,8 @@ function createUpdateCheck() {
112113

113114
function Page() {
114115
const { rawOptions, setOptions } = useRecordingOptions();
116+
const currentRecording = createCurrentRecordingQuery();
117+
const isRecording = () => !!currentRecording.data;
115118

116119
createUpdateCheck();
117120

@@ -389,27 +392,34 @@ function Page() {
389392
<TargetTypeButton
390393
selected={rawOptions.targetMode === "display"}
391394
Component={IconMdiMonitor}
392-
onClick={() =>
395+
disabled={isRecording()}
396+
onClick={() => {
397+
//if recording early return
398+
if (isRecording()) return;
393399
setOptions("targetMode", (v) =>
394400
v === "display" ? null : "display",
395-
)
396-
}
401+
);
402+
}}
397403
name="Display"
398404
/>
399405
<TargetTypeButton
400406
selected={rawOptions.targetMode === "window"}
401407
Component={IconLucideAppWindowMac}
402-
onClick={() =>
403-
setOptions("targetMode", (v) => (v === "window" ? null : "window"))
404-
}
408+
disabled={isRecording()}
409+
onClick={() => {
410+
if (isRecording()) return;
411+
setOptions("targetMode", (v) => (v === "window" ? null : "window"));
412+
}}
405413
name="Window"
406414
/>
407415
<TargetTypeButton
408416
selected={rawOptions.targetMode === "area"}
409417
Component={IconMaterialSymbolsScreenshotFrame2Rounded}
410-
onClick={() =>
411-
setOptions("targetMode", (v) => (v === "area" ? null : "area"))
412-
}
418+
disabled={isRecording()}
419+
onClick={() => {
420+
if (isRecording()) return;
421+
setOptions("targetMode", (v) => (v === "area" ? null : "area"));
422+
}}
413423
name="Area"
414424
/>
415425
</div>

apps/desktop/src/routes/in-progress-recording.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import {
99
createEffect,
1010
createMemo,
1111
createSignal,
12-
onCleanup,
1312
Show,
1413
} from "solid-js";
1514
import { createStore, produce } from "solid-js/store";
1615
import createPresence from "solid-presence";
16+
import { authStore } from "~/store";
1717
import { createTauriEventListener } from "~/utils/createEventListener";
1818
import {
1919
createCurrentRecordingQuery,
@@ -33,6 +33,8 @@ declare global {
3333
}
3434
}
3535

36+
const MAX_RECORDING_FOR_FREE = 5 * 60 * 1000;
37+
3638
export default function () {
3739
const [state, setState] = createSignal<State>(
3840
window.COUNTDOWN === 0
@@ -47,6 +49,7 @@ export default function () {
4749
const [time, setTime] = createSignal(Date.now());
4850
const currentRecording = createCurrentRecordingQuery();
4951
const optionsQuery = createOptionsQuery();
52+
const auth = authStore.createQuery();
5053

5154
const audioLevel = createAudioInputLevel();
5255

@@ -156,6 +159,33 @@ export default function () {
156159
return t;
157160
};
158161

162+
const isMaxRecordingLimitEnabled = () => {
163+
// Only enforce the limit on instant mode.
164+
// We enforce it on studio mode when exporting.
165+
return (
166+
optionsQuery.rawOptions.mode === "instant" &&
167+
// If the data is loaded and the user is not upgraded
168+
auth.data?.plan?.upgraded === false
169+
);
170+
};
171+
172+
let aborted = false;
173+
createEffect(() => {
174+
if (
175+
isMaxRecordingLimitEnabled() &&
176+
adjustedTime() > MAX_RECORDING_FOR_FREE &&
177+
!aborted
178+
) {
179+
aborted = true;
180+
stopRecording.mutate();
181+
}
182+
});
183+
184+
const remainingRecordingTime = () => {
185+
if (MAX_RECORDING_FOR_FREE < adjustedTime()) return 0;
186+
return MAX_RECORDING_FOR_FREE - adjustedTime();
187+
};
188+
159189
const [countdownRef, setCountdownRef] = createSignal<HTMLDivElement | null>(
160190
null,
161191
);
@@ -196,7 +226,12 @@ export default function () {
196226
>
197227
<IconCapStopCircle />
198228
<span class="font-[500] text-[0.875rem] tabular-nums">
199-
{formatTime(adjustedTime() / 1000)}
229+
<Show
230+
when={isMaxRecordingLimitEnabled()}
231+
fallback={formatTime(adjustedTime() / 1000)}
232+
>
233+
{formatTime(remainingRecordingTime() / 1000)}
234+
</Show>
200235
</span>
201236
</button>
202237

0 commit comments

Comments
 (0)