linked-data-browser 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ldo/profile.context.ts +14 -0
- package/.ldo/profile.typings.ts +6 -4
- package/app/index.tsx +2 -1
- package/components/ResourceView.tsx +7 -2
- package/components/common/LoadingBar.tsx +27 -0
- package/components/common/ProfileAvatar.tsx +28 -0
- package/components/nav/DialogProvider.tsx +5 -8
- package/components/nav/Layout.tsx +20 -81
- package/components/nav/header/AddressBox.tsx +8 -7
- package/components/nav/header/AvatarMenu.tsx +54 -57
- package/components/nav/header/Header.tsx +18 -2
- package/components/nav/header/SignInMenu.tsx +11 -14
- package/components/nav/header/ViewMenu.tsx +4 -4
- package/components/sharing/AccessDropdown.tsx +95 -0
- package/components/sharing/CopyLink.tsx +21 -0
- package/components/sharing/PermissionRow.tsx +38 -0
- package/components/sharing/SharingModal.tsx +149 -0
- package/components/sharing/WacRuleForm.tsx +44 -0
- package/components/sharing/agentPermissions/AgentInformation.tsx +37 -0
- package/components/sharing/agentPermissions/AgentInput.tsx +126 -0
- package/components/sharing/agentPermissions/AgentPermissionRow.tsx +36 -0
- package/components/sharing/agentPermissions/AgentPermissions.tsx +56 -0
- package/components/sharing/agentPermissions/useContactFilter.ts +35 -0
- package/components/ui/button.tsx +52 -5
- package/components/ui/dialog.tsx +1 -1
- package/components/ui/input-dropdown.tsx +105 -0
- package/components/ui/input.tsx +34 -2
- package/components/ui/text.tsx +47 -0
- package/components/useViewContext.tsx +141 -0
- package/components/{nav/utilityResourceViews → utilityResourceViews}/ErrorMessageResourceView.tsx +2 -2
- package/lib/icons/Fingerprint.tsx +4 -0
- package/lib/icons/Link.tsx +4 -0
- package/lib/icons/Loader.tsx +4 -0
- package/lib/icons/LogOut.tsx +4 -0
- package/lib/icons/Plus.tsx +4 -0
- package/lib/icons/Save.tsx +4 -0
- package/lib/icons/User.tsx +4 -0
- package/lib/icons/UserPlus.tsx +4 -0
- package/lib/icons/Users.tsx +4 -0
- package/package.json +12 -7
- package/resourceViews/Container/ContainerView.tsx +5 -6
- package/resourceViews/Profile/ProfileConfig.tsx +20 -0
- package/resourceViews/Profile/ProfileKnows.tsx +65 -0
- package/resourceViews/Profile/ProfileView.tsx +59 -0
- package/resourceViews/RawCode/RawCodeView.tsx +35 -9
- package/test-server/server-config.json +1 -1
- package/components/nav/useValidView.tsx +0 -51
|
@@ -13,7 +13,7 @@ import { View } from 'react-native';
|
|
|
13
13
|
import type { ViewRef } from '@rn-primitives/types';
|
|
14
14
|
import { cn } from '../../../lib/utils';
|
|
15
15
|
import { ViewIcon } from '../../../lib/icons/ViewIcon';
|
|
16
|
-
import {
|
|
16
|
+
import { useViewContext } from '../../useViewContext';
|
|
17
17
|
import { ResourceViewConfig } from '../../../components/ResourceView';
|
|
18
18
|
|
|
19
19
|
export const ViewMenu: FunctionComponent = () => {
|
|
@@ -24,7 +24,7 @@ export const ViewMenu: FunctionComponent = () => {
|
|
|
24
24
|
left: 12,
|
|
25
25
|
right: 12,
|
|
26
26
|
};
|
|
27
|
-
const { validViews } =
|
|
27
|
+
const { validViews } = useViewContext();
|
|
28
28
|
|
|
29
29
|
const [isOpen, setIsOpen] = React.useState<string>();
|
|
30
30
|
|
|
@@ -63,7 +63,7 @@ const ListItem = React.forwardRef<
|
|
|
63
63
|
}
|
|
64
64
|
>(({ className, viewConfig, ...props }, ref) => {
|
|
65
65
|
// TODO: add navigationn to `href` on `NavigationMenuLink` onPress
|
|
66
|
-
const { curViewConfig, setCurViewConfig } =
|
|
66
|
+
const { curViewConfig, setCurViewConfig } = useViewContext();
|
|
67
67
|
const Icon = viewConfig.displayIcon;
|
|
68
68
|
return (
|
|
69
69
|
<View role="listitem">
|
|
@@ -78,7 +78,7 @@ const ListItem = React.forwardRef<
|
|
|
78
78
|
{...props}
|
|
79
79
|
>
|
|
80
80
|
<Icon size={20} />
|
|
81
|
-
<Text className="
|
|
81
|
+
<Text className="ml-2" size="sm">
|
|
82
82
|
{viewConfig.displayName}
|
|
83
83
|
</Text>
|
|
84
84
|
</NavigationMenuLink>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { AccessModeList } from '@ldo/connected-solid';
|
|
3
|
+
import { FunctionComponent } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuGroup,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from '../ui/dropdown-menu';
|
|
13
|
+
import { Text } from '../ui/text';
|
|
14
|
+
import { Button } from '../ui/button';
|
|
15
|
+
import { Checkbox } from '../ui/checkbox';
|
|
16
|
+
import { ChevronDown } from '../../lib/icons/ChevronDown';
|
|
17
|
+
import { View } from 'react-native';
|
|
18
|
+
|
|
19
|
+
interface AccessDropdownProps {
|
|
20
|
+
value: AccessModeList;
|
|
21
|
+
onChange: (newAccess: AccessModeList) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const AccessDropdown: FunctionComponent<AccessDropdownProps> = ({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
}) => {
|
|
28
|
+
const accessDescription = useMemo(() => {
|
|
29
|
+
if (value.read && value.append && value.write && value.control) {
|
|
30
|
+
return 'Owner';
|
|
31
|
+
} else if (value.read && value.append && value.write && !value.control) {
|
|
32
|
+
return 'Editor';
|
|
33
|
+
} else if (value.read && value.append && !value.write && !value.control) {
|
|
34
|
+
return 'Contributor';
|
|
35
|
+
} else if (value.read && !value.append && !value.write && !value.control) {
|
|
36
|
+
return 'Viewer';
|
|
37
|
+
} else if (!value.read && !value.append && !value.write && !value.control) {
|
|
38
|
+
return 'No Access';
|
|
39
|
+
} else {
|
|
40
|
+
return 'Custom';
|
|
41
|
+
}
|
|
42
|
+
}, [value.append, value.control, value.read, value.write]);
|
|
43
|
+
|
|
44
|
+
const onChangeAccessMode = useCallback(
|
|
45
|
+
(field: string, newValue: boolean) => {
|
|
46
|
+
onChange({
|
|
47
|
+
...value,
|
|
48
|
+
[field]: newValue,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
[onChange, value],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<DropdownMenu>
|
|
56
|
+
<DropdownMenuTrigger asChild>
|
|
57
|
+
<Button variant="outline" className=" w-[130px] justify-between gap-0">
|
|
58
|
+
<Text>{accessDescription}</Text>
|
|
59
|
+
<Text>
|
|
60
|
+
<ChevronDown size={14} />
|
|
61
|
+
</Text>
|
|
62
|
+
</Button>
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
<DropdownMenuContent>
|
|
65
|
+
<DropdownMenuLabel>Permissions</DropdownMenuLabel>
|
|
66
|
+
<DropdownMenuSeparator />
|
|
67
|
+
<DropdownMenuGroup>
|
|
68
|
+
{['read', 'append', 'write', 'control'].map((accessModeName) => (
|
|
69
|
+
<DropdownMenuItem
|
|
70
|
+
key={accessModeName}
|
|
71
|
+
closeOnPress={false}
|
|
72
|
+
onPress={() =>
|
|
73
|
+
onChangeAccessMode(
|
|
74
|
+
accessModeName,
|
|
75
|
+
!value[accessModeName as keyof AccessModeList],
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
<Checkbox
|
|
80
|
+
checked={value[accessModeName as keyof AccessModeList]}
|
|
81
|
+
onCheckedChange={(newVal) =>
|
|
82
|
+
onChangeAccessMode(accessModeName, newVal)
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
<Text>
|
|
86
|
+
{accessModeName.charAt(0).toUpperCase() +
|
|
87
|
+
accessModeName.slice(1)}
|
|
88
|
+
</Text>
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
))}
|
|
91
|
+
</DropdownMenuGroup>
|
|
92
|
+
</DropdownMenuContent>
|
|
93
|
+
</DropdownMenu>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Button } from '../ui/button';
|
|
3
|
+
import { Text } from '../ui/text';
|
|
4
|
+
import { Link } from '../../lib/icons/Link';
|
|
5
|
+
import { FunctionComponent } from 'react';
|
|
6
|
+
import { useViewContext } from '../useViewContext';
|
|
7
|
+
|
|
8
|
+
export const CopyLink: FunctionComponent = () => {
|
|
9
|
+
const { targetUri } = useViewContext();
|
|
10
|
+
const [message, setMessage] = useState('Copy Link');
|
|
11
|
+
const onCopy = useCallback(async () => {
|
|
12
|
+
await navigator.clipboard.writeText(targetUri ?? '');
|
|
13
|
+
setMessage('Copied');
|
|
14
|
+
await new Promise((res) => setTimeout(res, 2000));
|
|
15
|
+
setMessage('Copy Link');
|
|
16
|
+
}, [targetUri]);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Button variant="outline" className="flex-row" onPress={onCopy} text={message} iconLeft={<Link />} />
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AccessModeList } from '@ldo/connected-solid';
|
|
3
|
+
import { Text } from '../ui/text';
|
|
4
|
+
import { FunctionComponent } from 'react';
|
|
5
|
+
import { AccessDropdown } from './AccessDropdown';
|
|
6
|
+
import { View } from 'react-native';
|
|
7
|
+
import { Avatar, AvatarFallback } from 'components/ui/avatar';
|
|
8
|
+
import { LucideIcon } from 'lucide-react-native';
|
|
9
|
+
|
|
10
|
+
interface PermissionRowProps {
|
|
11
|
+
Icon: LucideIcon;
|
|
12
|
+
displayName: string;
|
|
13
|
+
value: AccessModeList;
|
|
14
|
+
onChange: (newValue: AccessModeList) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PermissionRow: FunctionComponent<PermissionRowProps> = ({
|
|
18
|
+
Icon,
|
|
19
|
+
displayName,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
}) => {
|
|
23
|
+
return (
|
|
24
|
+
<View className="flex-row justify-between items-center">
|
|
25
|
+
<View className="flex-row gap-4 flex-1 items-center">
|
|
26
|
+
<Avatar alt={displayName}>
|
|
27
|
+
<AvatarFallback>
|
|
28
|
+
<Text>
|
|
29
|
+
<Icon />
|
|
30
|
+
</Text>
|
|
31
|
+
</AvatarFallback>
|
|
32
|
+
</Avatar>
|
|
33
|
+
<Text bold>{displayName}</Text>
|
|
34
|
+
</View>
|
|
35
|
+
<AccessDropdown value={value} onChange={onChange} />
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
FunctionComponent,
|
|
4
|
+
PropsWithChildren,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import {
|
|
12
|
+
Dialog,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from '../ui/dialog';
|
|
18
|
+
import { Button } from '../ui/button';
|
|
19
|
+
import { Text } from '../ui/text';
|
|
20
|
+
import { CopyLink } from './CopyLink';
|
|
21
|
+
import {
|
|
22
|
+
GetWacRuleError,
|
|
23
|
+
GetWacRuleSuccess,
|
|
24
|
+
SolidContainer,
|
|
25
|
+
SolidLeaf,
|
|
26
|
+
WacRule,
|
|
27
|
+
} from '@ldo/connected-solid';
|
|
28
|
+
import { useViewContext } from '../useViewContext';
|
|
29
|
+
import { LoadingBar } from '../common/LoadingBar';
|
|
30
|
+
import { ScrollView } from 'react-native';
|
|
31
|
+
import { WacRuleForm } from './WacRuleForm';
|
|
32
|
+
import { isEqual } from 'lodash';
|
|
33
|
+
|
|
34
|
+
interface SharingModalMethods {
|
|
35
|
+
openSharingModal: () => void;
|
|
36
|
+
closeSharingModal: () => void;
|
|
37
|
+
isModalOpen: boolean;
|
|
38
|
+
}
|
|
39
|
+
const sharingModalContext = createContext<SharingModalMethods>({
|
|
40
|
+
openSharingModal: () => {},
|
|
41
|
+
closeSharingModal: () => {},
|
|
42
|
+
isModalOpen: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const useSharingModal = () => {
|
|
46
|
+
return useContext(sharingModalContext);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const SharingModalProvider: FunctionComponent<PropsWithChildren<{}>> = ({
|
|
50
|
+
children,
|
|
51
|
+
}) => {
|
|
52
|
+
const { targetResource } = useViewContext();
|
|
53
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
54
|
+
const [wacResult, setWacResult] = useState<
|
|
55
|
+
| GetWacRuleError<SolidLeaf | SolidContainer>
|
|
56
|
+
| GetWacRuleSuccess<SolidLeaf | SolidContainer>
|
|
57
|
+
| undefined
|
|
58
|
+
>();
|
|
59
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
60
|
+
const [editedRules, setEditedRules] = useState<WacRule>({
|
|
61
|
+
public: { read: false, write: false, append: false, control: false },
|
|
62
|
+
authenticated: { read: false, write: false, append: false, control: false },
|
|
63
|
+
agent: {},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (
|
|
68
|
+
targetResource &&
|
|
69
|
+
targetResource.type !== 'InvalidIdentifierResource' &&
|
|
70
|
+
isOpen
|
|
71
|
+
) {
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
|
|
74
|
+
targetResource.getWac().then((wac) => {
|
|
75
|
+
setWacResult(
|
|
76
|
+
wac as
|
|
77
|
+
| GetWacRuleError<SolidLeaf | SolidContainer>
|
|
78
|
+
| GetWacRuleSuccess<SolidLeaf | SolidContainer>,
|
|
79
|
+
);
|
|
80
|
+
if (wac.type === 'getWacRuleSuccess') {
|
|
81
|
+
setEditedRules(wac.wacRule);
|
|
82
|
+
}
|
|
83
|
+
setIsLoading(false);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}, [targetResource, isOpen]);
|
|
87
|
+
|
|
88
|
+
const didEdit = useMemo(() => {
|
|
89
|
+
if (wacResult?.type !== 'getWacRuleSuccess') return false;
|
|
90
|
+
return !isEqual(editedRules, wacResult.wacRule);
|
|
91
|
+
}, [editedRules, wacResult]);
|
|
92
|
+
|
|
93
|
+
const context = useMemo(
|
|
94
|
+
() => ({
|
|
95
|
+
isModalOpen: isOpen,
|
|
96
|
+
openSharingModal: () => setIsOpen(true),
|
|
97
|
+
closeSharingModal: () => setIsOpen(false),
|
|
98
|
+
}),
|
|
99
|
+
[isOpen],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const onApplyChanges = useCallback(async () => {
|
|
103
|
+
if (
|
|
104
|
+
didEdit &&
|
|
105
|
+
(targetResource?.type === 'SolidContainer' ||
|
|
106
|
+
targetResource?.type === 'SolidLeaf')
|
|
107
|
+
) {
|
|
108
|
+
setIsLoading(true);
|
|
109
|
+
const result = await targetResource.setWac(editedRules);
|
|
110
|
+
// TODO throw error with toast
|
|
111
|
+
setIsLoading(false);
|
|
112
|
+
}
|
|
113
|
+
setIsOpen(false);
|
|
114
|
+
}, [editedRules, targetResource, didEdit]);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<sharingModalContext.Provider value={context}>
|
|
118
|
+
<Dialog open={isOpen} onOpenChange={(value) => setIsOpen(value)}>
|
|
119
|
+
<DialogContent className="sm:w-[640px] w-[95vw] h-[95vh]">
|
|
120
|
+
<LoadingBar isLoading={isLoading} />
|
|
121
|
+
<DialogHeader>
|
|
122
|
+
<DialogTitle>Resource Sharing Preferences</DialogTitle>
|
|
123
|
+
</DialogHeader>
|
|
124
|
+
<ScrollView className="flex-1 border-border border-b border-t pt-2 pb-2 ml-[-24px] mr-[-24px] pl-6 pr-6">
|
|
125
|
+
{(() => {
|
|
126
|
+
if (!wacResult) return <></>;
|
|
127
|
+
if (wacResult.isError) {
|
|
128
|
+
return (
|
|
129
|
+
<Text className="color-red-800">{wacResult.message}</Text>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<WacRuleForm value={editedRules} onChange={setEditedRules} />
|
|
134
|
+
);
|
|
135
|
+
})()}
|
|
136
|
+
</ScrollView>
|
|
137
|
+
<DialogFooter>
|
|
138
|
+
<CopyLink />
|
|
139
|
+
<Button
|
|
140
|
+
text={didEdit ? 'Apply Changes' : 'Done'}
|
|
141
|
+
onPress={onApplyChanges}
|
|
142
|
+
/>
|
|
143
|
+
</DialogFooter>
|
|
144
|
+
</DialogContent>
|
|
145
|
+
</Dialog>
|
|
146
|
+
{children}
|
|
147
|
+
</sharingModalContext.Provider>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Separator } from '../ui/separator';
|
|
3
|
+
import { FunctionComponent } from 'react';
|
|
4
|
+
import { AgentPermissions } from './agentPermissions/AgentPermissions';
|
|
5
|
+
import { WacRule } from '@ldo/connected-solid';
|
|
6
|
+
import { View } from 'react-native';
|
|
7
|
+
import { PermissionRow } from './PermissionRow';
|
|
8
|
+
import { Users } from '../../lib/icons/Users';
|
|
9
|
+
import { Fingerprint } from '../../lib/icons/Fingerprint';
|
|
10
|
+
|
|
11
|
+
interface WacRuleFormProps {
|
|
12
|
+
value: WacRule;
|
|
13
|
+
onChange: (newWacRule: WacRule) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const WacRuleForm: FunctionComponent<WacRuleFormProps> = ({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<View className="gap-4 mt-2 mb-2">
|
|
22
|
+
<PermissionRow
|
|
23
|
+
displayName="Public Access"
|
|
24
|
+
Icon={Users}
|
|
25
|
+
value={value.public}
|
|
26
|
+
onChange={(newRule) => onChange({ ...value, public: newRule })}
|
|
27
|
+
/>
|
|
28
|
+
<Separator />
|
|
29
|
+
<PermissionRow
|
|
30
|
+
displayName="Authenticated Agents"
|
|
31
|
+
Icon={Fingerprint}
|
|
32
|
+
value={value.authenticated}
|
|
33
|
+
onChange={(newRule) => onChange({ ...value, authenticated: newRule })}
|
|
34
|
+
/>
|
|
35
|
+
<Separator />
|
|
36
|
+
<AgentPermissions
|
|
37
|
+
value={value.agent}
|
|
38
|
+
onChange={(newAgentRules) =>
|
|
39
|
+
onChange({ ...value, agent: newAgentRules })
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
</View>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SolidProfileShapeShapeType } from '.ldo/profile.shapeTypes';
|
|
2
|
+
import { useResource, useSolidAuth, useSubject } from '@ldo/solid-react';
|
|
3
|
+
import React, { FunctionComponent, ReactNode } from 'react';
|
|
4
|
+
import { View } from 'react-native';
|
|
5
|
+
import { ProfileAvatar } from 'components/common/ProfileAvatar';
|
|
6
|
+
import { Text } from '../../ui/text';
|
|
7
|
+
|
|
8
|
+
interface AgentInformationProps {
|
|
9
|
+
webId: string;
|
|
10
|
+
accessoryRight?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AgentInformation: FunctionComponent<AgentInformationProps> = ({
|
|
14
|
+
webId,
|
|
15
|
+
accessoryRight,
|
|
16
|
+
}) => {
|
|
17
|
+
const { session } = useSolidAuth();
|
|
18
|
+
useResource(webId);
|
|
19
|
+
const agentProfile = useSubject(SolidProfileShapeShapeType, webId);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View className="flex-row gap-4 flex-1 items-center">
|
|
23
|
+
<ProfileAvatar profile={agentProfile} />
|
|
24
|
+
<View className="flex-1">
|
|
25
|
+
<Text>
|
|
26
|
+
{agentProfile['@id'] === session.webId
|
|
27
|
+
? 'You'
|
|
28
|
+
: (agentProfile.fn ?? agentProfile.name ?? 'Unnamed Agent')}
|
|
29
|
+
</Text>
|
|
30
|
+
<Text size="xs" muted>
|
|
31
|
+
{webId}
|
|
32
|
+
</Text>
|
|
33
|
+
</View>
|
|
34
|
+
{accessoryRight && <View>{accessoryRight}</View>}
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { FunctionComponent } from 'react';
|
|
3
|
+
import { Pressable } from 'react-native';
|
|
4
|
+
import { useLinkQuery, useSolidAuth } from '@ldo/solid-react';
|
|
5
|
+
import { SolidProfileShapeShapeType } from '../../../.ldo/profile.shapeTypes';
|
|
6
|
+
import { AgentInformation } from './AgentInformation';
|
|
7
|
+
import { InputDropdown } from '../../ui/input-dropdown';
|
|
8
|
+
import { useContactFilter } from './useContactFilter';
|
|
9
|
+
import { Plus } from '../../../lib/icons/Plus';
|
|
10
|
+
|
|
11
|
+
interface AgentInputProps {
|
|
12
|
+
onAddAgent: (webId: string) => void;
|
|
13
|
+
existingAgents: string[];
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Component for dropdown items with profile data
|
|
18
|
+
const ContactDropdownItem: FunctionComponent<{
|
|
19
|
+
webId: string;
|
|
20
|
+
onSelect: (webId: string) => void;
|
|
21
|
+
}> = ({ webId, onSelect }) => {
|
|
22
|
+
return (
|
|
23
|
+
<Pressable
|
|
24
|
+
className="p-2 border-b border-border last:border-b-0 hover:bg-accent active:bg-accent cursor-pointer"
|
|
25
|
+
onPress={() => onSelect(webId)}
|
|
26
|
+
>
|
|
27
|
+
<AgentInformation webId={webId} />
|
|
28
|
+
</Pressable>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const friendsLinkQuery = {
|
|
33
|
+
knows: {
|
|
34
|
+
'@id': true,
|
|
35
|
+
fn: true,
|
|
36
|
+
},
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export const AgentInput: FunctionComponent<AgentInputProps> = ({
|
|
40
|
+
onAddAgent,
|
|
41
|
+
existingAgents,
|
|
42
|
+
className,
|
|
43
|
+
}) => {
|
|
44
|
+
const { session } = useSolidAuth();
|
|
45
|
+
const [inputValue, setInputValue] = useState('');
|
|
46
|
+
|
|
47
|
+
// Get current user's profile to access their "knows" list
|
|
48
|
+
const currentUserProfile = useLinkQuery(
|
|
49
|
+
SolidProfileShapeShapeType,
|
|
50
|
+
session.webId!,
|
|
51
|
+
session.webId!,
|
|
52
|
+
friendsLinkQuery,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Filter out contacts that are already in the individual agents section
|
|
56
|
+
const availableContacts = useMemo(() => {
|
|
57
|
+
if (!currentUserProfile?.knows) return [];
|
|
58
|
+
|
|
59
|
+
return currentUserProfile.knows
|
|
60
|
+
.filter((contact) => !existingAgents.includes(contact['@id']))
|
|
61
|
+
.map((contact) => contact['@id']);
|
|
62
|
+
}, [currentUserProfile?.knows, existingAgents]);
|
|
63
|
+
|
|
64
|
+
// Use the custom hook for filtering
|
|
65
|
+
const filteredContacts = useContactFilter(
|
|
66
|
+
availableContacts,
|
|
67
|
+
inputValue,
|
|
68
|
+
currentUserProfile?.knows ? Array.from(currentUserProfile.knows) : [],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleInputSubmit = useCallback(() => {
|
|
72
|
+
if (inputValue.trim()) {
|
|
73
|
+
// Check if it's a valid URI
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(inputValue);
|
|
76
|
+
onAddAgent(url.toString());
|
|
77
|
+
setInputValue('');
|
|
78
|
+
} catch {
|
|
79
|
+
// Do nothing
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [inputValue, onAddAgent]);
|
|
83
|
+
|
|
84
|
+
const handleContactSelect = useCallback(
|
|
85
|
+
(webId: string) => {
|
|
86
|
+
onAddAgent(webId);
|
|
87
|
+
setInputValue('');
|
|
88
|
+
},
|
|
89
|
+
[onAddAgent],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Filter function for the dropdown (now just returns the filtered contacts)
|
|
93
|
+
const filterContacts = useCallback(
|
|
94
|
+
(_contacts: string[], _searchText: string) => {
|
|
95
|
+
return filteredContacts;
|
|
96
|
+
},
|
|
97
|
+
[filteredContacts],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Render function for dropdown items
|
|
101
|
+
const renderContactItem = useCallback(
|
|
102
|
+
(webId: string, onSelect: (webId: string) => void) => (
|
|
103
|
+
<ContactDropdownItem webId={webId} onSelect={onSelect} />
|
|
104
|
+
),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<InputDropdown
|
|
110
|
+
placeholder="Add Contact or WebId"
|
|
111
|
+
value={inputValue}
|
|
112
|
+
onChangeText={setInputValue}
|
|
113
|
+
onSubmitEditing={handleInputSubmit}
|
|
114
|
+
items={availableContacts}
|
|
115
|
+
renderItem={renderContactItem}
|
|
116
|
+
filterItems={filterContacts}
|
|
117
|
+
buttonRight={{
|
|
118
|
+
iconRight: <Plus />,
|
|
119
|
+
onPress: handleInputSubmit,
|
|
120
|
+
variant: 'secondary',
|
|
121
|
+
}}
|
|
122
|
+
onItemSelect={handleContactSelect}
|
|
123
|
+
className={className}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AccessModeList } from '@ldo/connected-solid';
|
|
3
|
+
import { Text } from '../../ui/text';
|
|
4
|
+
import { FunctionComponent } from 'react';
|
|
5
|
+
import { AgentInformation } from './AgentInformation';
|
|
6
|
+
import { AccessDropdown } from '../AccessDropdown';
|
|
7
|
+
import { View } from 'react-native';
|
|
8
|
+
import { Button } from '../../ui/button';
|
|
9
|
+
import { Trash } from '../../../lib/icons/Trash';
|
|
10
|
+
|
|
11
|
+
interface AgentPermissionRowProps {
|
|
12
|
+
webId: string;
|
|
13
|
+
value: AccessModeList;
|
|
14
|
+
onChange: (newValue: AccessModeList | undefined) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const AgentPermissionRow: FunctionComponent<AgentPermissionRowProps> = ({
|
|
18
|
+
webId,
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<View className="flex-row justify-between align-center">
|
|
24
|
+
<AgentInformation webId={webId} />
|
|
25
|
+
<View className="flex-row">
|
|
26
|
+
<Button
|
|
27
|
+
variant="ghost"
|
|
28
|
+
onPress={() => onChange(undefined)}
|
|
29
|
+
className="w-10"
|
|
30
|
+
iconLeft={<Trash size={14} />}
|
|
31
|
+
/>
|
|
32
|
+
<AccessDropdown value={value} onChange={onChange} />
|
|
33
|
+
</View>
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { WacRule } from '@ldo/connected-solid';
|
|
3
|
+
import { FunctionComponent } from 'react';
|
|
4
|
+
import { AgentPermissionRow } from './AgentPermissionRow';
|
|
5
|
+
import { Text } from '../../ui/text';
|
|
6
|
+
import { AgentInput } from './AgentInput';
|
|
7
|
+
|
|
8
|
+
interface AgentPermissionsProps {
|
|
9
|
+
value: WacRule['agent'];
|
|
10
|
+
onChange: (newValue: WacRule['agent']) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AgentPermissions: FunctionComponent<AgentPermissionsProps> = ({
|
|
14
|
+
value,
|
|
15
|
+
onChange,
|
|
16
|
+
}) => {
|
|
17
|
+
const addAgent = useCallback(
|
|
18
|
+
(webId: string) => {
|
|
19
|
+
onChange({
|
|
20
|
+
...value,
|
|
21
|
+
[webId]: { read: false, write: false, append: false, control: false },
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
[value, onChange],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Text bold>Individual Agents</Text>
|
|
30
|
+
<AgentInput
|
|
31
|
+
onAddAgent={addAgent}
|
|
32
|
+
existingAgents={Object.keys(value)}
|
|
33
|
+
className="z-[999]"
|
|
34
|
+
/>
|
|
35
|
+
{Object.entries(value).map(([webId, accessModeList]) => (
|
|
36
|
+
<AgentPermissionRow
|
|
37
|
+
key={webId}
|
|
38
|
+
webId={webId}
|
|
39
|
+
value={accessModeList}
|
|
40
|
+
onChange={(newAccessModeList) => {
|
|
41
|
+
if (!newAccessModeList) {
|
|
42
|
+
const newVal = { ...value };
|
|
43
|
+
delete newVal[webId];
|
|
44
|
+
onChange(newVal);
|
|
45
|
+
} else {
|
|
46
|
+
onChange({
|
|
47
|
+
...value,
|
|
48
|
+
[webId]: newAccessModeList,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useContactFilter(
|
|
4
|
+
webIds: string[],
|
|
5
|
+
searchText: string,
|
|
6
|
+
contactsWithNames: Array<{ '@id': string; fn?: string }>,
|
|
7
|
+
) {
|
|
8
|
+
// Filter contacts based on search text (URI and name)
|
|
9
|
+
const filteredContacts = useMemo(() => {
|
|
10
|
+
if (!searchText.trim()) return webIds;
|
|
11
|
+
|
|
12
|
+
const lowerInput = searchText.toLowerCase();
|
|
13
|
+
|
|
14
|
+
return webIds.filter((webId) => {
|
|
15
|
+
// Check if the URI matches
|
|
16
|
+
if (webId.toLowerCase().includes(lowerInput)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check if the name matches
|
|
21
|
+
const contact = contactsWithNames.find((c) => c['@id'] === webId);
|
|
22
|
+
if (
|
|
23
|
+
contact &&
|
|
24
|
+
contact.fn &&
|
|
25
|
+
contact.fn.toLowerCase().includes(lowerInput)
|
|
26
|
+
) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
});
|
|
32
|
+
}, [webIds, searchText, contactsWithNames]);
|
|
33
|
+
|
|
34
|
+
return filteredContacts;
|
|
35
|
+
}
|