Skip to content

Commit

Permalink
Privacy mode (actualbudget#1272)
Browse files Browse the repository at this point in the history
  • Loading branch information
joel-jeremy authored Jul 18, 2023
1 parent 01659d5 commit 790b18d
Show file tree
Hide file tree
Showing 29 changed files with 838 additions and 510 deletions.
143 changes: 143 additions & 0 deletions packages/desktop-client/src/components/PrivacyFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, {
useState,
useCallback,
Children,
type ComponentPropsWithRef,
type ReactNode,
} from 'react';

import usePrivacyMode from 'loot-core/src/client/privacy';

import useFeatureFlag from '../hooks/useFeatureFlag';
import { useResponsive } from '../ResponsiveProvider';

import { View } from './common';

export type ConditionalPrivacyFilterProps = {
children: ReactNode;
privacyFilter?: boolean | PrivacyFilterProps;
defaultPrivacyFilterProps?: PrivacyFilterProps;
};
export function ConditionalPrivacyFilter({
children,
privacyFilter,
defaultPrivacyFilterProps,
}: ConditionalPrivacyFilterProps) {
let renderPrivacyFilter = (children, mergedProps) => (
<PrivacyFilter {...mergedProps}>{children}</PrivacyFilter>
);
return privacyFilter ? (
typeof privacyFilter === 'boolean' ? (
<PrivacyFilter {...defaultPrivacyFilterProps}>{children}</PrivacyFilter>
) : (
renderPrivacyFilter(
children,
mergeConditionalPrivacyFilterProps(
defaultPrivacyFilterProps,
privacyFilter,
),
)
)
) : (
<>{Children.toArray(children)}</>
);
}

type PrivacyFilterProps = ComponentPropsWithRef<typeof View> & {
activationFilters?: (boolean | (() => boolean))[];
blurIntensity?: number;
};
export default function PrivacyFilter({
activationFilters,
blurIntensity,
children,
...props
}: PrivacyFilterProps) {
let privacyModeFeatureFlag = useFeatureFlag('privacyMode');
let privacyMode = usePrivacyMode();
// Limit mobile support for now.
let { isNarrowWidth } = useResponsive();
let activate =
privacyMode &&
!isNarrowWidth &&
(!activationFilters ||
activationFilters.every(value =>
typeof value === 'boolean' ? value : value(),
));

let blurAmount = blurIntensity != null ? `${blurIntensity}px` : '3px';

return !privacyModeFeatureFlag || !activate ? (
<>{Children.toArray(children)}</>
) : (
<BlurredOverlay blurIntensity={blurAmount} {...props}>
{children}
</BlurredOverlay>
);
}

function BlurredOverlay({ blurIntensity, children, ...props }) {
let [hovered, setHovered] = useState(false);
let onHover = useCallback(() => setHovered(true), [setHovered]);
let onHoverEnd = useCallback(() => setHovered(false), [setHovered]);

let blurStyle = {
...(!hovered && {
filter: `blur(${blurIntensity})`,
WebkitFilter: `blur(${blurIntensity})`,
}),
};

let { style, ...restProps } = props;

return (
<View
style={{
display: style?.display ? style.display : 'inline-flex',
...blurStyle,
...style,
}}
onPointerEnter={onHover}
onPointerLeave={onHoverEnd}
{...restProps}
>
{children}
</View>
);
}
export function mergeConditionalPrivacyFilterProps(
defaultPrivacyFilterProps: PrivacyFilterProps = {},
privacyFilter: ConditionalPrivacyFilterProps['privacyFilter'],
): ConditionalPrivacyFilterProps['privacyFilter'] {
if (privacyFilter == null || privacyFilter === false) {
return privacyFilter;
}

if (privacyFilter === true) {
return defaultPrivacyFilterProps;
}

return merge(defaultPrivacyFilterProps, privacyFilter);
}

function merge(initialValue, ...objects) {
return objects.reduce((prev, current) => {
Object.keys(current).forEach(key => {
const pValue = prev[key];
const cValue = current[key];

if (Array.isArray(pValue) && Array.isArray(cValue)) {
prev[key] = pValue.concat(...cValue);
} else if (isObject(pValue) && isObject(cValue)) {
prev[key] = merge(pValue, cValue);
} else {
prev[key] = cValue;
}
});
return prev;
}, initialValue);
}

function isObject(obj) {
return obj && typeof obj === 'object';
}
68 changes: 51 additions & 17 deletions packages/desktop-client/src/components/Titlebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { listen } from 'loot-core/src/platform/client/fetch';
import useFeatureFlag from '../hooks/useFeatureFlag';
import ArrowLeft from '../icons/v1/ArrowLeft';
import AlertTriangle from '../icons/v2/AlertTriangle';
import SvgEye from '../icons/v2/Eye';
import SvgEyeSlashed from '../icons/v2/EyeSlashed';
import NavigationMenu from '../icons/v2/NavigationMenu';
import { useResponsive } from '../ResponsiveProvider';
import { colors } from '../style';
Expand All @@ -39,7 +41,7 @@ import {
import { useSidebar } from './FloatableSidebar';
import LoggedInUser from './LoggedInUser';
import { useServerURL } from './ServerContext';
import SheetValue from './spreadsheet/SheetValue';
import useSheetValue from './spreadsheet/useSheetValue';

export let TitlebarContext = createContext();

Expand All @@ -65,23 +67,43 @@ export function TitlebarProvider({ children }) {
}

function UncategorizedButton() {
let count = useSheetValue(queries.uncategorizedCount());
return (
<SheetValue binding={queries.uncategorizedCount()}>
{node => {
const num = node.value;
return (
num !== 0 && (
<ButtonLink
bare
to="/accounts/uncategorized"
style={{ color: colors.r5 }}
>
{num} uncategorized {num === 1 ? 'transaction' : 'transactions'}
</ButtonLink>
)
);
}}
</SheetValue>
count !== 0 && (
<ButtonLink
bare
to="/accounts/uncategorized"
style={{ color: colors.r5 }}
>
{count} uncategorized {count === 1 ? 'transaction' : 'transactions'}
</ButtonLink>
)
);
}

function PrivacyButton({ localPrefs, onTogglePrivacy }) {
let [isPrivacyEnabled, setIsPrivacyEnabled] = useState(
localPrefs.isPrivacyEnabled,
);
let togglePrivacy = () => {
setIsPrivacyEnabled(!isPrivacyEnabled);
onTogglePrivacy(!isPrivacyEnabled);
};

let privacyIconStyle = {
width: 23,
height: 23,
color: 'inherit',
};

return (
<Button bare onClick={togglePrivacy}>
{isPrivacyEnabled ? (
<SvgEyeSlashed style={privacyIconStyle} />
) : (
<SvgEye style={privacyIconStyle} />
)}
</Button>
);
}

Expand Down Expand Up @@ -257,6 +279,7 @@ function BudgetTitlebar({ globalPrefs, saveGlobalPrefs, localPrefs }) {
function Titlebar({
globalPrefs,
saveGlobalPrefs,
savePrefs,
localPrefs,
userData,
floatingSidebar,
Expand All @@ -271,6 +294,11 @@ function Titlebar({
let { isNarrowWidth } = useResponsive();
const serverURL = useServerURL();

let privacyModeFeatureFlag = useFeatureFlag('privacyMode');
let onTogglePrivacy = enabled => {
savePrefs({ isPrivacyEnabled: enabled });
};

return isNarrowWidth ? null : (
<View
style={[
Expand Down Expand Up @@ -351,6 +379,12 @@ function Titlebar({
</Routes>
<View style={{ flex: 1 }} />
<UncategorizedButton />
{privacyModeFeatureFlag && (
<PrivacyButton
localPrefs={localPrefs}
onTogglePrivacy={onTogglePrivacy}
/>
)}
{serverURL ? (
<SyncButton
style={{ marginLeft: 10 }}
Expand Down
8 changes: 7 additions & 1 deletion packages/desktop-client/src/components/accounts/Balance.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSelectedItems } from '../../hooks/useSelected';
import ArrowButtonRight1 from '../../icons/v2/ArrowButtonRight1';
import { colors } from '../../style';
import { View, Text, Button } from '../common';
import PrivacyFilter from '../PrivacyFilter';
import CellValue from '../spreadsheet/CellValue';
import format from '../spreadsheet/format';
import useSheetValue from '../spreadsheet/useSheetValue';
Expand All @@ -24,7 +25,9 @@ function DetailedBalance({ name, balance }) {
}}
>
{name}{' '}
<Text style={{ fontWeight: 600 }}>{format(balance, 'financial')}</Text>
<PrivacyFilter>
<Text style={{ fontWeight: 600 }}>{format(balance, 'financial')}</Text>
</PrivacyFilter>
</Text>
);
}
Expand Down Expand Up @@ -134,6 +137,9 @@ export function Balances({
getStyle={value => ({
color: value < 0 ? colors.r5 : value > 0 ? colors.g5 : colors.n8,
})}
privacyFilter={{
blurIntensity: 5,
}}
/>

<ArrowButtonRight1
Expand Down
Loading

0 comments on commit 790b18d

Please sign in to comment.