Skip to content

Commit

Permalink
Notifications (#26)
Browse files Browse the repository at this point in the history
* Added base POC to listen for push notifications on a service-worker;

* Base upload and fetch updates for multi payload with default key;

* Fixed FileProvider with better handling of the new result structure; Fixed useSiteData with correct fallback if static files fail;

* Cleaned up the uploadFileMetaData additionalThumbnails;

* Cleaned up uploadFile with multi-payloads and only support for Blobs;

* Fixed thumbs;

* Clean out old fileMetaData properties;

* Updated getting thumb and payload to use the payloadKey; Updated uploads to support payloads with corresponding thumbs;

* Fixes and updates on new thumb approach;

* Made contacts multi payload;

* Updated fileBrowser to support downloading the first payload image;

* In Progress migration of attributes to use multi payload;

* In Progress migration of attributes to use multi payload;

* Fixes and updates to have the attributes work with their multi payload data

* Contact source fixes;

* Cleanup imports;

* Expanded useTinyThumb with Key to get the right thumbnail sizes from the meta

* Improved attribute editor; Fixed imageSelector to show new Blob when image selected;

* Added base multi payload support to the posts;

* Updated article composer; And UI components to be compatible with the new hooks

* TODO: Fixes;

* Added support for the media with an embeddedPost;

* Added multi payload for comments;

* Added multi payload for comments;

* Extra null checks on the serverMetadata;

* Fixed multi payload multi files previewThumbnail fetching;

* Add support for editing a post;

* Got base articleComposer working again;

* Got base articleComposer working again;

* Added support for using the updatePost from savePost; Added adding and/or removing payloads on the updatePost handler;

* Better post update and save handling;

* Better post update and save handling;

* Improved dynamic data fetching error "handling";

* Better fileBrowser;

* Better fileBrowser;

* Added new byGlobalTransitId functions

* Improved OdinImage structure;

* Made the images work again over transit when on the feed;

* Made the images work again over transit when on the feed;

* More null checks for a potentially null ServermetaData;

* Fixed processing state updates;

* Added upload state for media when already in the SocialFeed;

* Added CommentMedia preview;

* Build fixes;

* Updated comment media teaser;

* Added systemFileType support to OdinImage; Fixed Embedded Post with globalTransitId;

* Updated static file handling with default payloads being included;

* Removed wrong/old useage of DEFAULT_PAYLOAD_KEY;

* Added a "Show on your feed" checkbox for channels;

* Fixed external Files from the post to still work;

* Made usePost always use save function;

* Made articles have their payloads on the postFile;

* Added lastModified into the OdinImage props for cache busting;

* Added apex domain info on DNS Settings View;

* Unified asserts on file providers;

* Added lastModified into the OdinImage props for cache busting;

* Cleanup YourInfo & YourSignature;

* Updated ProfileCardProvider to avoid a 404 on a thumb request when the profile image is an SVG;

* Improved svg handling on OdinImage

* Added video's into the PostFile payloads;

* Add support for big articles on updatePost;

* Made saveAttr update header/append payloads instead of always download and re-uploading payloads;

* Improved attribute upload handling;

* Moved push notification management into a hook with an owner specific PushProvider;

* Added notification devices settings view;

* Fix deps;

* Improved attribute file type;

* Refactored AttributeFile to DriveSearchResult type;

* Clearly indicate when changing encryption state fails;

* Add support for re-uploading attribute when their encrypt / unencrypt changes;

* Reset usage to senderOdinId on Feed Drive posts;

* Changed dependency location;

* Changed dependency location for some more;

* Fixed YourSignature error;

* Refactored PostFile<T> to DriveSearchResult<T>

* Import fix;

* Bumped version of libs; Added extra trigger for the multi-payload branch;

* Cleanup unused deps;

* Bump lib version(s)

* Added extra check on the contact source provider for undefined attr;

* Fixes for non-existent contentType;

* Updated endpoint used to fetch the application server key;

* Addec chat-app;

* Reorganized types;

* Fiex isExternal check on PostTeaserCard;

* Simplified upload file objects; Added base chat provider implementations; Base UI work for a chat screen;

* Bump lib version(s)

* Updated sw;

* WIP POC;

* Fixed base64ToUint8Array;

* WIP POC;

---------

Co-authored-by: github-actions <[email protected]>
  • Loading branch information
stef-coenen and github-actions authored Nov 29, 2023
1 parent f043be7 commit eb20ca4
Show file tree
Hide file tree
Showing 14 changed files with 3,655 additions and 239 deletions.
3,358 changes: 3,125 additions & 233 deletions package-lock.json

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions packages/chat-app/src/templates/Conversations/Conversations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
ActionButton,
ConnectionImage,
ConnectionName,
EmojiSelector,
FileSelector,
ImageIcon,
Input,
OwnerName,
VolatileInput,
t,
useDotYouClient,
useSiteData,
} from '@youfoundation/common-app';
import { useConversations } from '../../hooks/chat/useConversations';
import { DriveSearchResult } from '@youfoundation/js-lib/core';
import { Conversation } from '../../providers/ConversationProvider';
import { useEffect, useState } from 'react';
import { OdinImage } from '@youfoundation/ui-lib';
import { HomePageConfig } from '@youfoundation/js-lib/public';
import { BuiltInProfiles, GetTargetDriveFromProfileId } from '@youfoundation/js-lib/profile';

const ConversationsOverview = () => {
return (
<div className="flex h-screen w-full flex-row overflow-hidden">
<div className="h-screen w-full max-w-xs flex-shrink-0 border-r bg-page-background p-5 ">
<ProfileHeader />
<ConversationsList />
</div>
<div className="h-screen w-full flex-grow bg-background">
<Chat />
</div>
</div>
);
};

const ProfileHeader = () => {
const { data } = useSiteData();
const { getIdentity, getDotYouClient } = useDotYouClient();
const dotYouClient = getDotYouClient();
const odinId = getIdentity() || undefined;

return (
<div className="flex flex-row items-center gap-2 pb-5">
<OdinImage
dotYouClient={dotYouClient}
targetDrive={GetTargetDriveFromProfileId(BuiltInProfiles.StandardProfileId)}
fileId={data?.owner.profileImageFileId}
fileKey={data?.owner.profileImageFileKey}
lastModified={data?.owner.profileImageLastModified}
previewThumbnail={data?.owner.profileImagePreviewThumbnail}
className="aspect-square max-h-[2.5rem] w-full max-w-[2.5rem] rounded-full border border-neutral-200"
fit="cover"
odinId={odinId}
/>
<OwnerName />
</div>
);
};

const ConversationsList = () => {
const [isSearchActive, setIsSearchActive] = useState(false);
const { data: conversations } = useConversations().all;

const flatConversaions =
(conversations?.pages
?.flatMap((page) => page.searchResults)
?.filter(Boolean) as DriveSearchResult<Conversation>[]) || [];

return (
<div className="flex flex-grow flex-col ">
<SearchConversation setIsSearchActive={setIsSearchActive} />
<div className="flex-grow overflow-auto">
{flatConversaions?.map((conversation) => (
<ConversationItem key={conversation.fileId} conversation={conversation} />
))}
</div>
</div>
);
};

const ConversationItem = ({ conversation }: { conversation: DriveSearchResult<Conversation> }) => {
return <>{conversation.fileMetadata.appData.content.conversationId}</>;
};

const SearchConversation = ({
setIsSearchActive,
}: {
setIsSearchActive: (isActive: boolean) => void;
}) => {
const [query, setQuery] = useState<string | undefined>(undefined);

useEffect(() => {
if (query && query.length > 1) setIsSearchActive(true);
else setIsSearchActive(false);
}, [query]);

return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="flex flex-row gap-1">
<Input onChange={(e) => setQuery(e.target.value)} />
<ActionButton type="secondary">{t('Search')}</ActionButton>
</div>
</form>
);
};

const Chat = ({ conversationId }: { conversationId: string }) => {
return (
<div className="flex h-full flex-grow flex-col">
<ChatHeader />
<ChatHistory />
<ChatComposer />
</div>
);
};

const ChatHeader = () => {
return (
<div className="flex flex-row items-center gap-2 bg-page-background p-5 ">
<ConnectionImage odinId="sam.dotyou.cloud" className="border border-neutral-200" size="sm" />
<ConnectionName odinId="sam.dotyou.cloud" />
</div>
);
};

const ChatHistory = () => {
return <div className="h-full flex-grow"></div>;
};

const ChatComposer = () => {
return (
<div className="flex flex-shrink-0 flex-row gap-2 bg-page-background px-5 py-3">
<div className="my-auto flex flex-row items-center gap-1">
<EmojiSelector
size="none"
className="px-1 py-1 text-foreground text-opacity-30 hover:text-opacity-100"
onInput={(val) => console.log(val)}
/>
<FileSelector
// onChange={(newFiles) => setAttachment(newFiles?.[0])}
className="text-foreground text-opacity-30 hover:text-opacity-100"
>
<ImageIcon className="h-5 w-5" />
</FileSelector>
</div>
<VolatileInput placeholder="Your message" className="rounded-md border bg-background p-2" />
<ActionButton type="secondary">{t('Send')}</ActionButton>
</div>
);
};

export default ConversationsOverview;
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const getSocialFeed = async (
const allPostFiles = (
await Promise.all(
result.searchResults.map(async (dsr) => {
const odinId = dsr.fileMetadata.senderOdinId || window.location.hostname;
const odinId = dsr.fileMetadata.senderOdinId;
return dsrToPostFile(dotYouClient, odinId, dsr, feedDrive, result.includeMetadataHeader);
})
)
Expand Down
1 change: 1 addition & 0 deletions packages/owner-app/dev-dist/registerSW.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/owner-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"cross-env": "^7.0.3",
"postcss": "8.4.31",
"prettier-plugin-tailwindcss": "0.5.5",
"tailwindcss": "3.3.3"
"tailwindcss": "3.3.3",
"vite-plugin-pwa": "^0.16.5"
},
"browserslist": {
"production": [
Expand Down
File renamed without changes
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { createPortal } from 'react-dom';
import {
ErrorNotification,
HardDrive,
SubtleMessage,
Times,
Trash,
t,
} from '@youfoundation/common-app';
import { usePortal } from '@youfoundation/common-app';
import { ActionButton } from '@youfoundation/common-app';
import { DialogWrapper } from '@youfoundation/common-app';
import { usePushNotificationClients } from '../../../hooks/notifications/usePushNotifications';
import { PushNotificationSubscription } from '../../../provider/notifications/PushProvider';

const PushNotificationsDialog = ({
isOpen,
onClose,
}: {
isOpen: boolean;

onClose: () => void;
}) => {
const target = usePortal('modal-container');
const { data: devices } = usePushNotificationClients().fetch;
const {
removeAll: {
mutate: removeAllDevices,
status: removeAllDevicesStatus,
error: removeAllDevicesError,
},
} = usePushNotificationClients();

if (!isOpen) return null;
const dialog = (
<DialogWrapper title={t('Push Notifications')} onClose={onClose}>
<ErrorNotification error={removeAllDevicesError} />
{!devices?.length ? (
<SubtleMessage>{t('No devices registered')}</SubtleMessage>
) : (
<>
<div className="grid grid-flow-col gap-4">
{devices?.map((device) => (
<DeviceView subscription={device} key={device.accessRegistrationId} />
))}
</div>
<div className="flex flex-row-reverse">
<ActionButton
type="remove"
icon={Trash}
onClick={() => removeAllDevices()}
state={removeAllDevicesStatus}
>
{t('Remove all')}
</ActionButton>
</div>
</>
)}
</DialogWrapper>
);

return createPortal(dialog, target);
};

const DeviceView = ({
subscription,
className,
}: {
subscription: PushNotificationSubscription;
className?: string;
}) => {
const {
fetchCurrent: { data: currentDevice },
removeCurrent: {
mutate: removeCurrentDevice,
status: removeCurrentDeviceStatus,
error: removeCurrentDeviceError,
},
} = usePushNotificationClients();

return (
<>
<ErrorNotification error={removeCurrentDeviceError} />
<div className={`flex flex-row items-center ${className ?? ''}`}>
<HardDrive className="mb-auto mr-3 mt-1 h-6 w-6" />
<div className="mr-2 flex flex-col">
{subscription.friendlyName}
<small className="block text-sm">
{t('Created')}: {new Date(subscription.subscriptionStartedDate).toLocaleDateString()}
</small>
</div>
{currentDevice?.accessRegistrationId === subscription.accessRegistrationId ? (
<ActionButton
icon={Times}
type="secondary"
size="square"
className="ml-2"
onClick={async () => {
removeCurrentDevice();
}}
state={removeCurrentDeviceStatus}
/>
) : null}
</div>
</>
);
};

export default PushNotificationsDialog;
87 changes: 87 additions & 0 deletions packages/owner-app/src/hooks/notifications/usePushNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useDotYouClient } from '@youfoundation/common-app';
import {
GetApplicationServerKey,
GetCurrentDeviceDetails,
GetRegisteredDevices,
RegisterNewDevice,
RemoveAllRegisteredDevice,
RemoveRegisteredDevice,
} from '../../provider/notifications/PushProvider';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

export const usePushNotifications = () => {
const dotYouClient = useDotYouClient().getDotYouClient();

// Register the push Application Server
// Use serviceWorker.ready to ensure that you can subscribe for push
return {
isEnabled: Notification.permission === 'granted',
enableOnThisDevice: async () => {
await Notification.requestPermission();
console.log('Notification permission granted.');
await navigator.serviceWorker.ready.then(async (serviceWorkerRegistration) => {
const publicKey = await GetApplicationServerKey();
console.log(publicKey);
const options = {
userVisibleOnly: true,
applicationServerKey: publicKey,
};

serviceWorkerRegistration.pushManager.subscribe(options).then(
async (pushSubscription) => {
await RegisterNewDevice(dotYouClient, pushSubscription);
alert('successfully registered');
},
(error) => {
console.error(error);
}
);
});
},
};
};

export const usePushNotificationClients = () => {
const queryClient = useQueryClient();
const dotYouClient = useDotYouClient().getDotYouClient();

const getCurrentClient = async () => {
return await GetCurrentDeviceDetails(dotYouClient);
};

const removeCurrentDevice = async () => {
return await RemoveRegisteredDevice(dotYouClient);
};

const getNotificationClients = async () => {
return await GetRegisteredDevices(dotYouClient);
};

const removeAllClients = async () => {
return await RemoveAllRegisteredDevice(dotYouClient);
};

return {
fetch: useQuery({
queryKey: ['notification-clients'],
queryFn: () => getNotificationClients(),
}),
removeAll: useMutation({
mutationFn: () => removeAllClients(),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['notification-clients'] });
},
}),

fetchCurrent: useQuery({
queryKey: ['notification-clients', 'current'],
queryFn: () => getCurrentClient(),
}),
removeCurrent: useMutation({
mutationFn: () => removeCurrentDevice(),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['notification-clients'], exact: false });
},
}),
};
};
6 changes: 6 additions & 0 deletions packages/owner-app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ root.render(
<App />
</React.StrictMode>
);

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(
import.meta.env.MODE === 'production' ? '/owner/sw.js' : '/owner/dev-sw.js?dev-sw'
);
}
Loading

0 comments on commit eb20ca4

Please sign in to comment.