Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sharing to contacts and revamp settings page #25

Merged
merged 16 commits into from
Feb 4, 2025
7 changes: 7 additions & 0 deletions app/global.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
/// <reference path="node_modules/@zeppos/device-types/dist/index.d.ts" />
/// <reference path="node_modules/@zeppos/zml/zml.d.ts" />

enum TabView {
Settings,
Contacts,
About,
}
42 changes: 42 additions & 0 deletions app/setting/components/TabContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ContactList } from './contactList';
import {
createAuthView,
createShareEmailInput,
createSignOutButton,
} from '../util/createViews';
import { useSettings } from '../context/SettingsContext';

// This file should be used to define the different tabs shown in settings.
// The tabs are defined as functions that return the pseudo-JSX for the tab content.

export const SettingsTab = () => {
const store = useSettings();
const isUserSignedIn = !!store.getAuthToken();
return View(
{ style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
[
isUserSignedIn ? createShareEmailInput() : createAuthView(),
isUserSignedIn && View({}, createSignOutButton()),
],
);
};

export const ContactsTab = () => {
const store = useSettings();
if (!store.getAuthToken()) {
console.error('No access token found');
return Text({}, 'No access token found. Please sign in first.');
}
const items = ContactList();
return items.length
? items
: Text({}, 'No contacts found. Add some in your Settings tab.');
};

export const AboutTab = () => Text({ style: { fontSize: '12px' } }, 'TODO');

export const TAB_COMPONENTS = {
Settings: SettingsTab,
Contacts: ContactsTab,
About: AboutTab,
};
22 changes: 21 additions & 1 deletion app/setting/components/button.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default PrimaryButton = ({ label, onClick }) => {
export const PrimaryButton = ({ label, onClick }) => {
return Button({
label,
onClick,
Expand All @@ -11,3 +11,23 @@ export default PrimaryButton = ({ label, onClick }) => {
},
});
};

export const XButton = (onClick) => {
return Button({
label: Image({ style: { width: '10px' }, src: xb64 }),
onClick,
style: {
background: '#FF0000',
borderRadius: '12px',
boxShadow: 'none',
color: '#FFF',
fontSize: '12px',
minWidth: '16px',
minHeight: '16px',
padding: '0',
},
});
};

const xb64 =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAA7EAAAOxAGVKw4bAAABdklEQVRIiZ2V3YrCMBBGi+xFKcUHK1L27cQLKRKWRUR8NFl63e/sRRONbTqNDgRCO7+HzExRRALsJHVAXbwpQA04SW3qZyGpAXpgAK5A9YbzytsM3kcDPBW887skIrnkBAEqSefYUNIdaIJCC/SSCAHCXdIN2C45l1RLui7Y9kBbAHtfWkoG4CZpVskES9JW0iEoujiLhLzg8jbnadaTCtzDBiiB44rBDdgyvpZr9D11OqCc8iwBt4bLHwuLmzmflO5C5kHeuD+xGM8uB1ceFiuIL3UJRT4WI0gurh8Ly8aIsWF8MXHQ2d07//oke6uJXhCx0IyW8/OSNwPXZTWINVty7ubsysAyMHbwZUVnjgs/co1ZhP9fS6qB32xcQA6WE9E7Z2zGLgsX6/Mn2USS1ppxAFwhqWVcc6lxbc4W7Gbsge+g2Ei6x2UCXSrzRJBS0nFi+wfsYqWCcVH3fDBbiGaXX5U74qUfKbbAwcJiBKkk7R9YvPwD3quO5XgK8UIAAAAASUVORK5CYII=';
48 changes: 48 additions & 0 deletions app/setting/components/contactList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useSettings } from '../context/SettingsContext';
import { removeFilePermissionById } from '../util/google';
import { XButton } from './button';

/**
* Creates a list of Contact elements to display
* @param {object} contacts list of contacts
* @returns {Array<Contact>} list of Contact elements to display
*/
export const ContactList = () => {
const settings = useSettings();
const accessToken = settings.getAuthToken();

if (!accessToken) {
console.error('No access token found');
return [];
}
const email = settings.getEmail();
const contacts = settings.getSetting(`contactsList_${email}`) || {};

const contactsMap = new Map(Object.entries(contacts));
const list = [];
for (const [contact, permissionId] of contactsMap) {
list.push(
Contact(contact, () => {
removeFilePermissionById(permissionId, accessToken).then((result) => {
if (result.success) {
console.log('Successfully removed contact:', contact);
contactsMap.delete(contact);
const updatedContacts = Object.fromEntries(contactsMap);
settings.setSetting(`contactsList_${email}`, updatedContacts);
} else {
console.error('Failed to remove contact:', contact);
}
});
}),
);
}

return list;
};

const Contact = (contact, onClick) => {
return View({ style: { display: 'flex', gap: '10px' } }, [
contact,
XButton(onClick),
]);
};
36 changes: 36 additions & 0 deletions app/setting/components/tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useSettings } from '../context/SettingsContext';

const tabs = ['Settings', 'Contacts', 'About'];

/**
* Create the tabs for the settings page
* @param {string} activeTab the currently active tab
* @param {*} setSetting function to set the active tab
* @returns
*/
export const Tabs = () => {
const store = useSettings();
const activeTab = store.getState().activeTab;
const tabButtons = tabs.map((tabName) => {
const isActive = tabName === activeTab;
return Tab(tabName, isActive, () => store.setState('activeTab', tabName));
});

return View({ style: { display: 'flex', marginBottom: '15px' } }, tabButtons);
};

const Tab = (label, isActive, onClick) => {
const btn = Button({
label,
onClick,
style: {
flex: '1',
boxShadow: 'none',
background: isActive ? '#000' : '#FFF',
color: isActive ? '#FFF' : '#000',
display: 'inline',
fontSize: '12px',
},
});
return btn;
};
25 changes: 25 additions & 0 deletions app/setting/components/textInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const Input = (label, placeholder, onChange) => {
return TextInput({
label,
placeholder,
onChange,
subStyle: {
border: 'thin rgba(0,0,0,0.1) solid',
borderRadius: '8px',
boxSizing: 'content-box',
color: '#000',
height: '.8em',
lineHeight: '1.5em',
marginTop: '-16px',
padding: '8px',
paddingTop: '1.2em',
},
labelStyle: {
color: '#555',
fontSize: '0.8em',
paddingLeft: '8px',
position: 'relative',
top: '0.2em',
},
});
};
6 changes: 6 additions & 0 deletions app/setting/components/toast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const VisibleToast = (message) => {
return Toast({
message,
visible: true,
});
};
41 changes: 41 additions & 0 deletions app/setting/context/SettingsContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* This file contains the settings context and the settings store.
* The settings store is used to manage the settings of the app
* and provide context to the settings components.
* @param {object} storage settingsStorage object
* @returns
*/
let globalSettingsStore = null;

export const createSettingsStore = ({
settings,
settingsStorage: settingsStore,
}) => {
const store = {
setState(key, value) {
settingsStore.setItem(key, value);
},
getState() {
return settings;
},
setSetting: (key, value) => settingsStore.setItem(key, value),
getSetting: (key) => settingsStore.getItem(key),
getAuthToken: () => {
const authData = JSON.parse(settingsStore.getItem('googleAuthData'));
return authData?.access_token || '';
},
getEmail: () => {
const authData = JSON.parse(settingsStore.getItem('googleAuthData'));
return authData?.email;
},
};
globalSettingsStore = store;
return store;
};

export const useSettings = () => {
if (!globalSettingsStore) {
throw new Error('Settings store must be initialized before use');
}
return globalSettingsStore;
};
Loading