Skip to content

Commit

Permalink
Community nye UI improvements (#511)
Browse files Browse the repository at this point in the history
* Added custom tooltip for reactions/status;

* Type fix;

* Using names instead of domains;

* Save back-up from before a collaborative edit;

* Save back-up from before a collaborative edit;

* Add virtualizer to the threads page

* WIP threads page;

* Added inline composers to the threads page;

* Added loading state for messagecomposer; Improved threads layout;

* Better handle fast editing of a sent message;

* Added status details dialog;

* Add options to restore original message when making a message private again;

* Restored activity page to see recent messages in a channel;

* UI Cleanup; Rename;

* Added cache buster queryString to manifest icons (owner & community);

* Layout fix;

* Fix for in thread message edits;

* Better support editing in one place only;
stef-coenen authored Jan 7, 2025
1 parent 1100afc commit fc4cc76
Showing 31 changed files with 544 additions and 237 deletions.
10 changes: 5 additions & 5 deletions packages/apps/community-app/index.html
Original file line number Diff line number Diff line change
@@ -5,11 +5,11 @@
<base href="/apps/community/" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="shortcut icon" href="/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png?v=202501" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png?v=202501" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png?v=202501" />
<link rel="manifest" href="/icons/site.webmanifest?v=202501" />
<link rel="shortcut icon" href="/icons/favicon.ico?v=202501" />
<meta name="theme-color" content="#ffffff" />

<title>Homebase | Community</title>
4 changes: 2 additions & 2 deletions packages/apps/community-app/public/icons/site.webmanifest
Original file line number Diff line number Diff line change
@@ -5,12 +5,12 @@
"scope": "/",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"src": "/icons/android-chrome-192x192.png?v=202501",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/android-chrome-512x512.png",
"src": "/icons/android-chrome-512x512.png?v=202501",
"sizes": "512x512",
"type": "image/png"
}
34 changes: 34 additions & 0 deletions packages/apps/community-app/src/app/App.css
Original file line number Diff line number Diff line change
@@ -38,3 +38,37 @@
.faded-scrollbar::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0);
}

[data-tooltip] {
position: relative;
}

[data-tooltip]:after {
content: attr(data-tooltip);
position: absolute;
left: 0;
bottom: calc(100% + 0.3rem); /* put it on the top */
background-color: rgba(var(--color-page-background));
border-radius: 0.2rem;
border: 1px solid rgba(var(--color-foreground) / 0.1);
font-size: 0.9rem;
padding: 0.05rem 0.2rem;
color: rgba(var(--color-foreground));
width: max-content;
opacity: 0;
-webkit-transition: opacity 0.3s ease-in-out;
pointer-events: none;

--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
[data-tooltip-dir='left']:after {
right: calc(100% + 0.3rem);
left: auto;
}

[data-tooltip]:hover:after {
opacity: 1;
}
26 changes: 9 additions & 17 deletions packages/apps/community-app/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -44,9 +44,9 @@ const CommunityChannelDetail = lazy(() =>
default: communityApp.CommunityChannelDetail,
}))
);
const CommunityCatchup = lazy(() =>
import('../templates/Community/CommunityCatchup').then((communityApp) => ({
default: communityApp.CommunityCatchup,
const CommunityChannelsCatchup = lazy(() =>
import('../templates/Community/CommunityChannelsCatchup').then((communityApp) => ({
default: communityApp.CommunityChannelsCatchup,
}))
);
const CommunityThreadsCatchup = lazy(() =>
@@ -119,20 +119,12 @@ function App() {
<Route path={'threads'} element={<CommunityThreadsCatchup />} />
<Route path={'later'} element={<CommunityLater />} />

{/* Items for 'all' */}
<Route path={'all'} element={<CommunityCatchup />} />
<Route path={'all/:chatMessageKey'} element={<CommunityCatchup />} />
<Route path={'all/:chatMessageKey/:mediaKey'} element={<CommunityCatchup />} />

{/* Items for 'all' within a thread */}
<Route path={'all/:threadKey/thread'} element={<CommunityCatchup />} />
<Route
path={'all/:threadKey/thread/:chatMessageKey'}
element={<CommunityCatchup />}
/>
{/* Items for 'activity' */}
<Route path={'activity'} element={<CommunityChannelsCatchup />} />
<Route path={'activity/:chatMessageKey'} element={<CommunityChannelsCatchup />} />
<Route
path={'all/:threadKey/thread/:chatMessageKey/:mediaKey'}
element={<CommunityCatchup />}
path={'activity/:chatMessageKey/:mediaKey'}
element={<CommunityChannelsCatchup />}
/>

{/* Items for 'channel' */}
@@ -206,7 +198,7 @@ function App() {
const CommunityRootRoute = () => {
const { odinKey, communityKey } = useParams();
return window.innerWidth > 1024 ? (
<Navigate to={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/all`} />
<Navigate to={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/activity`} />
) : null;
};

Original file line number Diff line number Diff line change
@@ -52,13 +52,13 @@ export const CommunityThread = memo(
className="p-2 xl:hidden"
size="none"
type="mute"
href={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'all'}`}
href={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'activity'}`}
>
<ChevronLeft className="h-5 w-5" />
</ActionLink>
{t('Thread')}
<ActionLink
href={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'all'}`}
href={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'activity'}`}
icon={Times}
size="none"
type="mute"
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ import { CommunityMessage } from '../../../../providers/CommunityMessageProvider
import { CommunityMetadata, Draft } from '../../../../providers/CommunityMetadataProvider';
import { CommunityChannel } from '../../../../providers/CommunityProvider';
import { ChannelPlugin } from '../RTEChannelDropdown/RTEChannelDropdownPlugin';
import { Mentionable } from '@homebase-id/rich-text-editor/src/components/plate-ui/mention-input-element';

const RichTextEditor = lazy(() =>
import('@homebase-id/rich-text-editor').then((rootExport) => ({
@@ -166,7 +167,7 @@ export const MessageComposer = ({
};

const { data: contacts } = useAllContacts(true);
const mentionables: { key: string; text: string }[] = useMemo(() => {
const mentionables: Mentionable[] = useMemo(() => {
const filteredContacts =
(contacts
?.filter(
@@ -176,15 +177,24 @@ export const MessageComposer = ({
contact.fileMetadata.appData.content.odinId
)
)
?.map((contact) =>
contact.fileMetadata.appData.content.odinId
? {
key: contact.fileMetadata.appData.content.odinId,
text: contact.fileMetadata.appData.content.odinId,
}
: undefined
)
.filter(Boolean) as { key: string; text: string }[]) || [];
?.map((contact) => {
const content = contact.fileMetadata.appData.content;
if (!content?.odinId) return;
const name =
content.name &&
(content.name.displayName ??
(content.name.givenName || content.name.surname
? `${content.name.givenName ?? ''} ${content.name.surname ?? ''}`
: undefined));

return {
key: `${content.odinId} (${name})`,
value: content.odinId,
text: content.odinId,
label: `${content.odinId} (${name})`,
};
})
.filter(Boolean) as Mentionable[]) || [];

filteredContacts.push({ key: '@channel', text: '@channel' });
return filteredContacts;
@@ -210,7 +220,11 @@ export const MessageComposer = ({
}
}}
>
<Suspense>
<Suspense
fallback={
<div className="relative h-[119px] w-full border-t bg-background px-2 pb-1 dark:border-slate-800 md:rounded-md md:border" />
}
>
<RichTextEditor
className="relative w-8 flex-grow border-t bg-background px-2 pb-1 dark:border-slate-800 md:rounded-md md:border"
contentClassName="max-h-[50vh] overflow-auto"
Original file line number Diff line number Diff line change
@@ -15,7 +15,10 @@ import {
} from '@homebase-id/common-app';
import { ImageSource, OdinPayloadImage, OdinPreviewImage } from '@homebase-id/ui-lib';
import { useNavigate, useParams } from 'react-router-dom';
import { CommunityMessage } from '../../../../providers/CommunityMessageProvider';
import {
BACKEDUP_PAYLOAD_KEY,
CommunityMessage,
} from '../../../../providers/CommunityMessageProvider';
import { getTargetDriveFromCommunityId } from '../../../../providers/CommunityDefinitionProvider';
import { Times, ArrowLeft, Arrow } from '@homebase-id/common-app/icons';

@@ -46,7 +49,7 @@ export const CommunityMediaGallery = ({
};

const allkeys = msg.fileMetadata.payloads
?.filter((pyld) => pyld.key !== DEFAULT_PAYLOAD_KEY)
?.filter((pyld) => pyld.key !== DEFAULT_PAYLOAD_KEY && pyld.key !== BACKEDUP_PAYLOAD_KEY)
?.map((p) => p.key);
const nextKey = allkeys?.[allkeys.indexOf(mediaKey) + 1];
const prevKey = allkeys?.[allkeys.indexOf(mediaKey) - 1];
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import { Triangle } from '@homebase-id/common-app/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { useMemo, useState } from 'react';
import {
BACKEDUP_PAYLOAD_KEY,
COMMUNITY_LINKS_PAYLOAD_KEY,
CommunityMessage,
} from '../../../../providers/CommunityMessageProvider';
@@ -34,7 +35,9 @@ export const CommunityMedia = ({
communityId: string;
msg: HomebaseFile<CommunityMessage> | NewHomebaseFile<CommunityMessage>;
}) => {
const payloads = msg.fileMetadata.payloads?.filter((pyld) => pyld.key !== DEFAULT_PAYLOAD_KEY);
const payloads = msg.fileMetadata.payloads?.filter(
(pyld) => pyld.key !== DEFAULT_PAYLOAD_KEY && pyld.key !== BACKEDUP_PAYLOAD_KEY
);
const isGallery = payloads && payloads.length >= 2;
const navigate = useNavigate();
const { odinKey, communityKey, channelKey } = useParams();
@@ -53,7 +56,7 @@ export const CommunityMedia = ({
fit={'contain'}
onClick={() =>
navigate(
`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey}/${msg.fileMetadata.appData.uniqueId}/${payloads[0].key}`
`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || msg.fileMetadata.appData.content.channelId}/${msg.fileMetadata.appData.uniqueId}/${payloads[0].key}`
)
}
previewThumbnail={isGallery ? undefined : msg.fileMetadata.appData.previewThumbnail}
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import {
ActionButton,
useContentFromPayload,
useLongPress,
ConnectionName,
} from '@homebase-id/common-app';
import { DEFAULT_PAYLOAD_KEY, HomebaseFile, RichText } from '@homebase-id/js-lib/core';
import {
@@ -88,7 +89,8 @@ export const CommunityMessageItem = memo(
const editInThreadMatch = useMatch(
`${COMMUNITY_ROOT_PATH}/:odinKey/:communityKey/:channelKey/:threadKey/thread/:chatMessageKey/edit`
);
const isEdit = !!isDetail && !!(editMatch || editInThreadMatch);
const isEdit =
!!isDetail && ((!!editMatch && !hideThreads) || (!!editInThreadMatch && hideThreads));

const navigate = useNavigate();
const extendedCommunityActions: CommunityActions | undefined = useMemo(() => {
@@ -97,7 +99,7 @@ export const CommunityMessageItem = memo(
...communityActions,
doEdit: () =>
navigate(
`${COMMUNITY_ROOT_PATH}/${community?.fileMetadata.senderOdinId}/${community?.fileMetadata.appData.uniqueId}/${channelKey}/${threadKey ? `${threadKey}/thread/` : ``}${msg.fileMetadata.appData.uniqueId}/edit`
`${COMMUNITY_ROOT_PATH}/${community?.fileMetadata.senderOdinId}/${community?.fileMetadata.appData.uniqueId}/${channelKey || msg.fileMetadata.appData.content.channelId}/${threadKey ? `${threadKey}/thread/` : ``}${msg.fileMetadata.appData.uniqueId}/edit`
),
};
}, []);
@@ -368,13 +370,14 @@ const MessageTextRenderer = ({
target="_blank"
rel="noreferrer noopener"
className="break-words text-primary hover:underline"
data-tooltip={`@${attributes.value.replaceAll('@', '')}`}
>
@{attributes.value.replaceAll('@', '')}
@<ConnectionName odinId={attributes.value.replaceAll('@', '')} />
</a>
);
else
return (
<span className="break-all text-primary">
<span className="break-all font-semibold" data-tooltip={attributes.value}>
@{attributes.value.replaceAll('@', '')}
</span>
);
@@ -520,7 +523,7 @@ const CommunityMessageThreadSummary = ({
return (
<Link
className="mr-auto flex w-full max-w-xs flex-row items-center gap-2 rounded-lg px-1 py-1 text-indigo-500 transition-colors hover:bg-background hover:shadow-sm"
to={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'all'}/${msg.fileMetadata.appData.uniqueId}/thread`}
to={`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || msg.fileMetadata.appData.content.channelId}/${msg.fileMetadata.appData.uniqueId}/thread`}
>
{uniqueSenders.map((sender) => (
<AuthorImage odinId={sender} key={sender} className="h-7 w-7" excludeLink={true} />
Original file line number Diff line number Diff line change
@@ -143,7 +143,7 @@ const ReactionButton = ({
<button
className={`flex flex-row items-center gap-2 rounded-3xl border bg-background px-2 py-[0.1rem] shadow-sm hover:bg-primary hover:text-primary-contrast ${myReaction ? 'border-primary bg-primary/10 dark:bg-primary/60' : 'border-transparent'}`}
{...clickProps}
title={`${authors?.length ? authors.join(', ') : ''} ${t('reacted with')} ${emoji.emoji}`}
data-tooltip={`${authors?.length ? authors.join(', ') : ''} ${t('reacted with')} ${emoji.emoji}`}
>
<p>{emoji.emoji}</p>
<p className="text-sm">{emoji.count}</p>
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityDefinition } from '../../../providers/CommunityDefinitionProvider';
import { useCommunityMetadata } from '../../../hooks/community/useCommunityMetadata';
import { ChannelWithRecentMessage } from '../../../hooks/community/channels/useCommunityChannelsWithRecentMessages';
import { CommunityHistory } from '../channel/CommunityHistory';
import { ActionButton, ActionLink, COMMUNITY_ROOT_PATH, t } from '@homebase-id/common-app';
import { useCallback } from 'react';
import { ActionLink, COMMUNITY_ROOT_PATH, t } from '@homebase-id/common-app';
import { ExternalLink } from '@homebase-id/common-app/icons';

export const CommunityChannelCatchup = ({
community,
@@ -14,65 +13,37 @@ export const CommunityChannelCatchup = ({
channel: ChannelWithRecentMessage;
}) => {
const communityId = community.fileMetadata.appData.uniqueId;
const {
single: { data: metadata },
update: { mutate: updateMeta, status: updateStatus },
} = useCommunityMetadata({ odinId: community.fileMetadata.senderOdinId, communityId });

const doMarkAsRead = useCallback(() => {
if (
!metadata ||
!channel.fileMetadata.appData.uniqueId ||
!channel.lastMessage?.fileMetadata.created ||
updateStatus === 'pending'
)
return;

const newMetadata = {
...metadata,
fileMetadata: {
...metadata.fileMetadata,
appData: {
...metadata.fileMetadata.appData,
content: {
...metadata.fileMetadata.appData.content,
channelLastReadTime: {
...metadata.fileMetadata.appData.content.channelLastReadTime,
[channel.fileMetadata.appData.uniqueId]: channel.lastMessage.fileMetadata.created,
},
},
},
},
};

updateMeta({ metadata: newMetadata });
}, []);
const channelLink = `${COMMUNITY_ROOT_PATH}/${community.fileMetadata.senderOdinId}/${communityId}/${channel.fileMetadata.appData.uniqueId}`;

return (
<div className="rounded-md border">
<div className="flex flex-row justify-between bg-slate-200 px-2 py-2 dark:bg-slate-800">
<ActionLink
type="mute"
size="none"
className="text-lg hover:underline"
href={`${COMMUNITY_ROOT_PATH}/${community.fileMetadata.senderOdinId}/${communityId}/${channel.fileMetadata.appData.uniqueId}`}
>
# {channel.fileMetadata.appData.content.title}
</ActionLink>
{metadata ? (
<ActionButton
<div className="pb-3 last-of-type:pb-0">
<div className="overflow-hidden rounded-md border bg-background">
<div className="flex flex-row justify-between bg-slate-200 px-2 py-2 dark:bg-slate-800">
<ActionLink
type="mute"
size="none"
type={'secondary'}
className="w-auto px-3 py-1 text-sm"
state={updateStatus}
onClick={doMarkAsRead}
className="text-lg hover:underline"
href={channelLink}
>
{t('Mark as read')}
</ActionButton>
) : null}
</div>
<div className="relative">
<CommunityHistory community={community} channel={channel} alignTop={true} onlyNew={true} />
# {channel.fileMetadata.appData.content.title}
</ActionLink>

<ActionLink href={channelLink} type="secondary" size="none" className="px-2 py-1 text-sm">
{t('Open channel')}
<ExternalLink className="ml-2 h-3 w-3" />
</ActionLink>
</div>
<div className="relative">
<CommunityHistory
community={community}
channel={channel}
alignTop={true}
maxShowOptions={{
count: 10,
targetLink: channelLink,
}}
/>
</div>
</div>
</div>
);
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityDefinition } from '../../../providers/CommunityDefinitionProvider';
import { CommunityHistory } from '../channel/CommunityHistory';
import { AuthorName, COMMUNITY_ROOT_PATH, FakeAnchor } from '@homebase-id/common-app';
import {
ActionLink,
AuthorName,
COMMUNITY_ROOT_PATH,
ErrorBoundary,
t,
} from '@homebase-id/common-app';
import { useCommunityMessage } from '../../../hooks/community/messages/useCommunityMessage';
import { useCommunityChannel } from '../../../hooks/community/channels/useCommunityChannel';
import { ThreadMeta } from '../../../hooks/community/threads/useCommunityThreads';
import React from 'react';
import React, { useState } from 'react';
import { MessageComposer } from '../Message/composer/MessageComposer';
import { ExternalLink } from '@homebase-id/common-app/icons';

export const CommunityThreadCatchup = ({
community,
@@ -15,6 +23,7 @@ export const CommunityThreadCatchup = ({
threadMeta: ThreadMeta;
}) => {
const communityId = community.fileMetadata.appData.uniqueId;
const [participants, setParticipants] = useState<string[] | null>();

const { data: channel } = useCommunityChannel({
odinId: community.fileMetadata.senderOdinId,
@@ -33,11 +42,9 @@ export const CommunityThreadCatchup = ({
if (!channel || !originMessage) return null;

return (
<div className="rounded-md border bg-background hover:shadow-md">
<FakeAnchor
href={`${COMMUNITY_ROOT_PATH}/${community.fileMetadata.senderOdinId}/${communityId}/${channel.fileMetadata.appData.uniqueId}/${threadMeta.threadId}/thread`}
>
<div className="flex flex-col bg-slate-200 px-2 py-2 dark:bg-slate-800">
<div className="overflow-hidden rounded-md border bg-background hover:shadow-md">
<div className="flex flex-row items-center bg-slate-200 px-2 py-2 dark:bg-slate-800">
<div className="flex flex-col">
<p className="text-lg"># {channel.fileMetadata.appData.content.title}</p>
<p className="text-sm text-slate-400">
{threadMeta.participants.map((participant, index) => (
@@ -56,16 +63,42 @@ export const CommunityThreadCatchup = ({
))}
</p>
</div>
<div className="ml-auto">
<ActionLink
href={`${COMMUNITY_ROOT_PATH}/${community.fileMetadata.senderOdinId}/${communityId}/${channel.fileMetadata.appData.uniqueId}/${threadMeta.threadId}/thread`}
type="secondary"
size="none"
className="px-2 py-1 text-sm"
>
{t('See full thread')}
<ExternalLink className="ml-2 h-3 w-3" />
</ActionLink>
</div>
</div>

<div className="pointer-events-none relative">
<CommunityHistory
<CommunityHistory
community={community}
channel={channel}
origin={originMessage}
setParticipants={setParticipants}
alignTop={true}
maxShowOptions={{
count: 10,
targetLink: `${COMMUNITY_ROOT_PATH}/${community.fileMetadata.senderOdinId}/${communityId}/${channel.fileMetadata.appData.uniqueId}/${threadMeta.threadId}/thread`,
}}
/>
<ErrorBoundary>
{originMessage ? (
<MessageComposer
community={community}
thread={originMessage}
channel={channel}
origin={originMessage}
alignTop={true}
key={threadMeta.threadId}
threadParticipants={participants || undefined}
className="mt-auto xl:mt-0"
/>
</div>
</FakeAnchor>
) : null}
</ErrorBoundary>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ import { CommunityMessageItem } from '../Message/item/CommunityMessageItem';
import { useCommunityMessages } from '../../../hooks/community/messages/useCommunityMessages';
import { CommunityActions } from './ContextMenu';
import { useCommunityMetadata } from '../../../hooks/community/useCommunityMetadata';
import { useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { stringGuidsEqual } from '@homebase-id/js-lib/helpers';

export const CommunityHistory = memo(
@@ -30,6 +30,7 @@ export const CommunityHistory = memo(
setIsEmptyChat?: (isEmpty: boolean) => void;
alignTop?: boolean;
onlyNew?: boolean;
maxShowOptions?: { count: number; targetLink: string };
emptyPlaceholder?: ReactNode;
setParticipants?: React.Dispatch<React.SetStateAction<string[] | null | undefined>>;
}) => {
@@ -41,6 +42,7 @@ export const CommunityHistory = memo(
setIsEmptyChat,
alignTop,
onlyNew,
maxShowOptions,
emptyPlaceholder,
setParticipants,
} = props;
@@ -78,7 +80,7 @@ export const CommunityHistory = memo(
maxAge: onlyNew ? lastReadTime : undefined,
});

const flattenedMsgs =
const [flattenedMsgs, isSliced] =
useMemo(() => {
const flat = (messages?.pages?.flatMap((page) => page?.searchResults)?.filter(Boolean) ||
[]) as HomebaseFile<CommunityMessage>[];
@@ -87,8 +89,10 @@ export const CommunityHistory = memo(
flat.push(origin as HomebaseFile<CommunityMessage>);
}

return flat;
}, [messages, origin]) || [];
if (!maxShowOptions) return [flat, false];
const maxShow = maxShowOptions.count;
return [flat.slice(0, maxShow), maxShow && flat.length > maxShow];
}, [messages, origin, maxShowOptions]) || [];

useEffect(() => {
if (setIsEmptyChat && isFetched && (!flattenedMsgs || flattenedMsgs.length === 0))
@@ -160,7 +164,12 @@ export const CommunityHistory = memo(
useEffect(() => {
const [lastItem] = virtualizer.getVirtualItems();
if (!lastItem) return;
if (lastItem.index >= flattenedMsgs?.length - 1 && hasMoreMessages && !isFetchingNextPage)
if (
lastItem.index >= flattenedMsgs?.length - 1 &&
hasMoreMessages &&
!isFetchingNextPage &&
!isSliced
)
fetchNextPage();
}, [
hasMoreMessages,
@@ -259,10 +268,19 @@ export const CommunityHistory = memo(
if (isLoaderRow) {
return (
<div key={item.key} data-index={item.index} ref={virtualizer.measureElement}>
{hasMoreMessages || isFetchingNextPage ? (
{(hasMoreMessages || isFetchingNextPage) && (!maxShowOptions || !isSliced) ? (
<div className="animate-pulse" key={'loading'}>
{t('Loading...')}
</div>
) : isSliced && maxShowOptions?.targetLink ? (
<div key={'end'} className="flex flex-row justify-center">
<Link
to={maxShowOptions?.targetLink}
className="rounded-full bg-page-background px-3 py-2 text-sm font-medium text-foreground opacity-50 hover:bg-primary hover:text-primary-contrast hover:opacity-100"
>
{t('See older messages')}
</Link>
</div>
) : null}
</div>
);
Original file line number Diff line number Diff line change
@@ -188,6 +188,8 @@ const CommunityContextActions = ({
const {
isCollaborative,
toggleCollaborative: { mutate: toggleCollaborative },
canBackup,
restoreAndMakePrivate: { mutate: restoreAndMakePrivate },
} = useCommunityCollaborativeMsg({ msg, community });

const { mutate: resend, error: resendError } = useCommunityMessage().update;
@@ -236,11 +238,38 @@ const CommunityContextActions = ({
});
}

optionalOptions.push({
icon: Persons,
label: isCollaborative ? t('Make private') : t('Make collaborative'),
onClick: () => toggleCollaborative(),
});
if (!isCollaborative) {
optionalOptions.push({
icon: Persons,
label: t('Make collaborative'),
onClick: () => toggleCollaborative(),
});
} else {
optionalOptions.push({
icon: Persons,
label: t('Make private'),
actionOptions: {
title: t('Make private'),
body: t(
'Are you sure you want to make this message private again, other collaborators will no longer be able to edit the message?'
),
type: 'info',
options: [
{
children: t('Make private'),
onClick: () => toggleCollaborative(),
},
canBackup
? {
children: t('Restore and make private'),
onClick: () => restoreAndMakePrivate(),
type: 'remove',
}
: undefined,
],
},
});
}
}

if (community)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ActionButton,
ConnectionName,
DialogWrapper,
EmojiSelector,
formatDateExludingYearIfCurrent,
@@ -17,6 +18,7 @@ import { useCommunity } from '../../../hooks/community/useCommunity';
import { useParams } from 'react-router-dom';
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityDefinition } from '../../../providers/CommunityDefinitionProvider';
import { CommunityStatus, setStatus } from '../../../providers/CommunityStatusProvider';

export const MyProfileStatus = ({ className }: { className?: string }) => {
const { odinKey, communityKey } = useParams();
@@ -26,7 +28,9 @@ export const MyProfileStatus = ({ className }: { className?: string }) => {
const {
get: { data: myStatus, isFetched },
} = useMyStatus({ community });
const hasStatus = myStatus?.emoji || myStatus?.status;
const hasStatus =
(myStatus?.emoji || myStatus?.status) &&
(!myStatus?.validTill || new Date(myStatus.validTill) > new Date());

if (!community || !isFetched) return null;

@@ -43,7 +47,8 @@ export const MyProfileStatus = ({ className }: { className?: string }) => {
e.stopPropagation();
setManageStatusDialogOpen(true);
}}
title={
data-tooltip-dir="left"
data-tooltip={
hasStatus
? `"${myStatus?.status || myStatus?.emoji}" ${myStatus?.validTill ? `${t('till')} ${formatDateExludingYearIfCurrent(new Date(myStatus.validTill))}` : ''}`
: undefined
@@ -63,6 +68,7 @@ export const MyProfileStatus = ({ className }: { className?: string }) => {
};

export const ProfileStatus = ({ odinId, className }: { odinId: string; className?: string }) => {
const [isStatusDialogOpen, setStatusDialogOpen] = useState(false);
const { odinKey, communityKey } = useParams();
const { data: community } = useCommunity({ odinId: odinKey, communityId: communityKey }).fetch;

@@ -76,16 +82,31 @@ export const ProfileStatus = ({ odinId, className }: { odinId: string; className
if (!community || !isFetched || !hasStatus) return null;

return (
<span
title={
hasStatus
? `"${myStatus?.status || myStatus?.emoji}" ${myStatus?.validTill ? `${t('till')} ${formatDateExludingYearIfCurrent(new Date(myStatus.validTill))}` : ''}`
: undefined
}
className={className}
>
{myStatus?.emoji || '💬'}
</span>
<>
<span
data-tooltip-dir="left"
data-tooltip={
hasStatus
? `"${myStatus?.status || myStatus?.emoji}" ${myStatus?.validTill ? `${t('till')} ${formatDateExludingYearIfCurrent(new Date(myStatus.validTill))}` : ''}`
: undefined
}
className={className}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setStatusDialogOpen(true);
}}
>
{myStatus?.emoji || '💬'}
</span>
{myStatus && isStatusDialogOpen ? (
<StatusDetailDialog
odinId={odinId}
status={myStatus}
onClose={() => setStatusDialogOpen(false)}
></StatusDetailDialog>
) : null}
</>
);
};

@@ -102,10 +123,12 @@ export const StatusDialog = ({
get: { data: myStatus, isFetched },
set: { mutate: setStatus, status: saveStatus },
} = useMyStatus({ community });
const [draftStatus, setDraftStatus] = useState(myStatus);
const [draftStatus, setDraftStatus] = useState<CommunityStatus | undefined>(
(myStatus && { ...myStatus, validTill: undefined }) || undefined
);

useEffect(() => {
if (isFetched) setDraftStatus(myStatus);
if (isFetched) setDraftStatus({ ...myStatus, validTill: undefined });
}, [isFetched, myStatus]);
useEffect(() => {
if (saveStatus === 'success') onClose();
@@ -122,7 +145,13 @@ export const StatusDialog = ({

setStatus({
community,
status: draftStatus,
status: {
...draftStatus,
validTill:
draftStatus.validTill && draftStatus.validTill > new Date().getTime()
? draftStatus.validTill
: undefined,
},
});
};

@@ -243,3 +272,46 @@ export const StatusDialog = ({

return createPortal(dialog, target);
};

const StatusDetailDialog = ({
status,
odinId,
onClose,
}: {
status: CommunityStatus;
odinId: string;
onClose: () => void;
}) => {
const target = usePortal('modal-container');
if (!status) return null;

const dialog = (
<div onClick={(e) => e.stopPropagation()}>
<DialogWrapper
title={
<>
{t('Status')}{' '}
<small className="block text-sm text-slate-400">
<ConnectionName odinId={odinId} />
</small>
</>
}
onClose={onClose}
isSidePanel={false}
isOverflowLess={true}
>
<div className="flex flex-row items-center gap-3">
<span className="text-4xl">{status.emoji || '💬'}</span>
<span>{status.status}</span>
</div>
{status.validTill ? (
<p className="mt-1 text-sm text-slate-400">
{t('untill')} {formatDateExludingYearIfCurrent(new Date(status.validTill))}
</p>
) : null}
</DialogWrapper>
</div>
);

return createPortal(dialog, target);
};
Original file line number Diff line number Diff line change
@@ -50,7 +50,6 @@ export const useCommunityPeerWebsocket = (
return;
}
isDebug && console.debug('[PeerCommunityWebsocket] Got notification', notification);

if (
(notification.notificationType === 'fileAdded' ||
notification.notificationType === 'fileModified' ||
Original file line number Diff line number Diff line change
@@ -50,8 +50,8 @@ export const getCommunityMessageQueryOptions = (
) => ({
queryKey: [
'community-message',
props?.communityId,
props?.messageId,
formatGuidId(props?.communityId),
formatGuidId(props?.messageId),
props?.fileSystemType?.toLowerCase() || 'standard',
],
queryFn: () =>
@@ -154,16 +154,21 @@ export const useCommunityMessage = (props?: {

newChat.fileMetadata.appData.previewThumbnail = uploadResult.previewThumbnail;
newChat.fileMetadata.appData.content.deliveryStatus = CommunityDeliveryStatus.Sent;
// We force set the keyHeader as it's returned from the upload, and needed for fast saves afterwards
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newChat.sharedSecretEncryptedKeyHeader = uploadResult.keyHeader as any;

return newChat;
};

const updateMessage = async ({
updatedChatMessage,
community,
storeBackup,
}: {
updatedChatMessage: HomebaseFile<CommunityMessage>;
community: HomebaseFile<CommunityDefinition>;
storeBackup?: boolean;
}) => {
const transformedMessage = {
...updatedChatMessage,
@@ -178,7 +183,13 @@ export const useCommunityMessage = (props?: {
);
}

await updateCommunityMessage(dotYouClient, community, transformedMessage);
await updateCommunityMessage(
dotYouClient,
community,
transformedMessage,
undefined,
storeBackup
);
};

return {
@@ -260,6 +271,7 @@ export const useCommunityMessage = (props?: {
},
payloads: msg?.fileMetadata.payloads,
},
sharedSecretEncryptedKeyHeader: newMessage.sharedSecretEncryptedKeyHeader,
};
}

@@ -394,16 +406,23 @@ export const updateCacheCommunityMessage = (
) => {
const currentData = queryClient.getQueryData<HomebaseFile<CommunityMessage>>([
'community-message',
communityId,

messageId,
formatGuidId(communityId),
formatGuidId(messageId),
fileSystemType?.toLowerCase() || 'standard',
]);
if (!currentData) return;

const updatedData = transformFn(currentData);
if (!updatedData) return;

queryClient.setQueryData(['community-message', messageId], updatedData);
queryClient.setQueryData(
[
'community-message',
formatGuidId(communityId),
formatGuidId(messageId),
fileSystemType?.toLowerCase() || 'standard',
],
updatedData
);
return currentData;
};
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ export const useEditLastMessageShortcut = ({
);
if (!myLastMessage) return;
navigate(
`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey}/${threadKey ? `${threadKey}/thread/` : ``}${myLastMessage?.fileMetadata.appData.uniqueId}/edit`
`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityKey}/${channelKey || 'activity'}/${threadKey ? `${threadKey}/thread/` : ``}${myLastMessage?.fileMetadata.appData.uniqueId}/edit`
);
}
},
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityMessage } from '../../providers/CommunityMessageProvider';
import { BACKEDUP_PAYLOAD_KEY, CommunityMessage } from '../../providers/CommunityMessageProvider';
import { useCommunityMessage } from './messages/useCommunityMessage';
import { CommunityDefinition } from '../../providers/CommunityDefinitionProvider';
import {
CommunityDefinition,
getTargetDriveFromCommunityId,
} from '../../providers/CommunityDefinitionProvider';
import { useContentFromPayload, useDotYouClientContext } from '@homebase-id/common-app';

export const useCommunityCollaborativeMsg = ({
msg,
@@ -10,8 +14,26 @@ export const useCommunityCollaborativeMsg = ({
msg: HomebaseFile<CommunityMessage>;
community?: HomebaseFile<CommunityDefinition>;
}) => {
const loggedInOdinId = useDotYouClientContext().getLoggedInIdentity();

const { mutate, ...updateProps } = useCommunityMessage().update;
const isCollaborative = msg.fileMetadata.appData.content.isCollaborative;
const isAuthor = msg.fileMetadata.senderOdinId === loggedInOdinId;
const { data: backedupData } = useContentFromPayload<CommunityMessage>(
isCollaborative &&
isAuthor &&
community &&
community.fileMetadata.appData.uniqueId &&
msg.fileId
? {
odinId: community?.fileMetadata.senderOdinId,
targetDrive: getTargetDriveFromCommunityId(community.fileMetadata.appData.uniqueId),
fileId: msg.fileId,
payloadKey: BACKEDUP_PAYLOAD_KEY,
systemFileType: msg.fileSystemType,
}
: undefined
);

return {
isCollaborative,
@@ -34,6 +56,31 @@ export const useCommunityCollaborativeMsg = ({
},
},
community,
storeBackup: !isCollaborative,
});
},
updateProps,
},
canBackup: isCollaborative && isAuthor && !!community && !!backedupData,
restoreAndMakePrivate: {
mutate: () => {
if (!msg || !community || !backedupData) return;

mutate({
updatedChatMessage: {
...msg,
fileMetadata: {
...msg.fileMetadata,
appData: {
...msg.fileMetadata.appData,
content: {
...backedupData,
isCollaborative: false,
},
},
},
},
community,
});
},
updateProps,
Original file line number Diff line number Diff line change
@@ -308,11 +308,14 @@ export const uploadCommunityMessage = async (
};
};

export const BACKEDUP_PAYLOAD_KEY = 'bckp_key';

export const updateCommunityMessage = async (
dotYouClient: DotYouClient,
community: HomebaseFile<CommunityDefinition>,
message: HomebaseFile<CommunityMessage>,
keyHeader?: KeyHeader
keyHeader?: KeyHeader,
storeBackup?: boolean
): Promise<void | UploadResult | UpdateResult> => {
const communityId = community.fileMetadata.appData.uniqueId as string;
const targetDrive = getTargetDriveFromCommunityId(communityId);
@@ -394,6 +397,14 @@ export const updateCommunityMessage = async (
});
}

if (storeBackup) {
payloads.push({
key: BACKEDUP_PAYLOAD_KEY,
payload: new Blob([payloadBytes], { type: 'application/json' }),
iv: getRandom16ByteArray(),
});
}

const updateResult = await patchFile(
dotYouClient,
encryptedKeyHeader,
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ import {
ChevronDown,
Bookmark,
ChatBubble,
RadioTower,
} from '@homebase-id/common-app/icons';
import { CommunityInfoDialog } from '../../components/Community/CommunityInfoDialog';
import { useConversationMetadata } from '@homebase-id/chat-app/src/hooks/chat/useConversationMetadata';
@@ -130,7 +131,7 @@ export const CommunityChannelNav = ({ isOnline }: { isOnline: boolean }) => {
communityId={communityKey}
setUnreadCount={setUnreadCountCallback}
/>
{/* <AllItem odinId={odinKey} communityId={communityKey} /> */}
<ActivityItem odinId={odinKey} communityId={communityKey} />
<LaterItem odinId={odinKey} communityId={communityKey} />
<PinsItem odinId={odinKey} communityId={communityKey} />
</div>
@@ -193,19 +194,19 @@ export const CommunityChannelNav = ({ isOnline }: { isOnline: boolean }) => {
);
};

// const AllItem = ({ odinId, communityId }: { odinId: string; communityId: string }) => {
// const href = `${COMMUNITY_ROOT_PATH}/${odinId}/${communityId}/all`;
// const isActive = !!useMatch({ path: href, end: true });
const ActivityItem = ({ odinId, communityId }: { odinId: string; communityId: string }) => {
const href = `${COMMUNITY_ROOT_PATH}/${odinId}/${communityId}/activity`;
const isActive = !!useMatch({ path: href, end: true });

// return (
// <Link
// to={href}
// className={`flex flex-row items-center gap-2 rounded-md px-2 py-1 ${isActive ? 'bg-primary/100 text-white' : `${!isTouchDevice() ? 'hover:bg-primary/10' : ''}`}`}
// >
// <RadioTower className="h-5 w-5" /> {t('Activity')}
// </Link>
// );
// };
return (
<Link
to={href}
className={`flex flex-row items-center gap-2 rounded-md px-2 py-1 ${isActive ? 'bg-primary/100 text-white' : `${!isTouchDevice() ? 'hover:bg-primary/10' : ''}`}`}
>
<RadioTower className="h-5 w-5" /> {t('Activity')}
</Link>
);
};

const ThreadItem = ({
odinId,
@@ -429,9 +430,9 @@ const DirectMessageItem = ({
>
<ConnectionImage odinId={recipient} size="xxs" className="flex-shrink-0" />
<span className="my-auto flex w-20 flex-grow flex-row flex-wrap items-center">
<p className="mr-1 leading-tight">
<span className="mr-1 leading-tight">
<ConnectionName odinId={recipient} />
</p>
</span>
{isYou ? (
<span className="text-sm leading-tight text-slate-400">{t('you')}</span>
) : (
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import { ChevronLeft, RadioTower } from '@homebase-id/common-app/icons';
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityDefinition } from '../../providers/CommunityDefinitionProvider';

export const CommunityCatchup = memo(() => {
export const CommunityChannelsCatchup = memo(() => {
const { odinKey, communityKey: communityId, threadKey } = useParams();
const { data: community, isFetched } = useCommunity({ odinId: odinKey, communityId }).fetch;

@@ -31,26 +31,17 @@ export const CommunityCatchup = memo(() => {
communityId: communityId,
}).fetch;

const channelsToCatchup = useMemo(
() =>
metadata &&
channels &&
channels?.filter((chnl) => {
if (!chnl.fileMetadata.appData.uniqueId) return false;
const lastReadTime =
metadata?.fileMetadata.appData.content.channelLastReadTime[
chnl.fileMetadata.appData.uniqueId
];
const channelsToCatchup = useMemo(() => {
if (!metadata || !channels) {
return [];
}

return (
chnl.lastMessage?.fileMetadata.created &&
chnl.lastMessage.fileMetadata.created > (lastReadTime || 0) &&
!!chnl.lastMessage.fileMetadata.senderOdinId &&
chnl.lastMessage.fileMetadata.senderOdinId !== loggedOnIdentity
);
}),
[channels, metadata, loggedOnIdentity]
);
return [...channels].sort(
(a, b) =>
(b.lastMessage?.fileMetadata.transitCreated || 0) -
(a.lastMessage?.fileMetadata.transitCreated || 0)
);
}, [channels, metadata, loggedOnIdentity]);

if (!community && isFetched)
return (
@@ -61,7 +52,7 @@ export const CommunityCatchup = memo(() => {

if (!community) {
return (
<div className="h-full w-20 flex-grow bg-background">
<div className="h-full w-20 flex-grow bg-page-background">
<LoadingBlock className="h-16 w-full" />
<div className="mt-8 flex flex-col gap-4 px-5">
<LoadingBlock className="h-16 w-full" />
@@ -76,15 +67,15 @@ export const CommunityCatchup = memo(() => {

return (
<ErrorBoundary>
<div className="h-full w-20 flex-grow bg-background">
<div className="h-full w-20 flex-grow bg-page-background">
<div className="relative flex h-full flex-row">
<div className="flex h-full flex-grow flex-col overflow-hidden">
<div className="flex h-full flex-grow flex-col">
<CommunityCatchupHeader community={community} />
{!channelsToCatchup?.length ? (
<p className="m-auto text-lg">{t('All done!')} 🎉</p>
) : (
<div className="flex h-20 flex-grow flex-col gap-3 overflow-auto p-3">
<div className="h-20 flex-grow overflow-auto p-3">
{channelsToCatchup?.map((chnl) => (
<CommunityChannelCatchup
community={community}
@@ -108,7 +99,7 @@ export const CommunityCatchup = memo(() => {
);
});

CommunityCatchup.displayName = 'CommunityCatchup';
CommunityChannelsCatchup.displayName = 'CommunityChannelsCatchup';

const CommunityCatchupHeader = ({
community,
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useCommunity } from '../../hooks/community/useCommunity';
import { ErrorBoundary, LoadingBlock, t, COMMUNITY_ROOT_PATH } from '@homebase-id/common-app';
import { Link, useParams } from 'react-router-dom';
import { CommunityThread } from '../../components/Community/CommunityThread';
import { memo } from 'react';
import { memo, useRef } from 'react';
import { ChatBubble, ChevronLeft } from '@homebase-id/common-app/icons';
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { CommunityDefinition } from '../../providers/CommunityDefinitionProvider';
import { useCommunityThreads } from '../../hooks/community/threads/useCommunityThreads';
import { CommunityThreadCatchup } from '../../components/Community/catchup/CommunityThreadCatchup';
import { useMarkCommunityAsRead } from '../../hooks/community/useMarkCommunityAsRead';
import { useVirtualizer } from '@tanstack/react-virtual';

export const CommunityThreadsCatchup = memo(() => {
const { odinKey, communityKey: communityId, threadKey } = useParams();
const scrollRef = useRef<HTMLDivElement>(null);
const { odinKey, communityKey: communityId } = useParams();
const { data: community, isFetched } = useCommunity({ odinId: odinKey, communityId }).fetch;

useMarkCommunityAsRead({ odinId: odinKey, communityId, threads: true });
@@ -21,9 +22,18 @@ export const CommunityThreadsCatchup = memo(() => {
communityId: communityId,
});

const virtualizer = useVirtualizer({
getScrollElement: () => scrollRef.current,
count: flatThreadMetas?.length || 0,
estimateSize: () => 500,
overscan: 5,
getItemKey: (index) => flatThreadMetas?.[index]?.threadId || index,
});
const items = virtualizer.getVirtualItems();

if (!isFetched || !fetchedThreads) {
return (
<div className="h-full w-20 flex-grow bg-background">
<div className="h-full w-20 flex-grow bg-page-background">
<LoadingBlock className="h-16 w-full" />
<div className="mt-8 flex flex-col gap-4 px-5">
<LoadingBlock className="h-16 w-full" />
@@ -36,7 +46,7 @@ export const CommunityThreadsCatchup = memo(() => {
);
}

if (!community || !flatThreadMetas)
if (!community)
return (
<div className="flex h-full flex-grow flex-col items-center justify-center">
<p className="text-4xl">Homebase Community</p>
@@ -53,24 +63,44 @@ export const CommunityThreadsCatchup = memo(() => {
{!flatThreadMetas?.length ? (
<p className="m-auto text-lg">{t('No threads found')}</p>
) : (
<div className="flex h-20 flex-grow flex-col gap-3 overflow-auto p-3">
{flatThreadMetas?.map((threadMeta) => (
<CommunityThreadCatchup
community={community}
threadMeta={threadMeta}
key={threadMeta.threadId}
/>
))}
<div className="relative h-20 flex-grow flex-col overflow-auto p-3" ref={scrollRef}>
<div
className="relative w-full overflow-hidden"
style={{
height: virtualizer.getTotalSize(),
}}
>
<div
className="absolute left-0 top-0 w-full"
style={{
transform: `translateY(${items[0]?.start - virtualizer.options.scrollMargin}px)`,
}}
>
{items.map((item) => {
const threadMeta = flatThreadMetas?.[item.index];
return (
<div
key={item.key}
data-index={item.index}
ref={virtualizer.measureElement}
className="pb-3 last-of-type:pb-0"
>
{threadMeta ? (
<CommunityThreadCatchup
community={community}
threadMeta={threadMeta}
key={threadMeta.threadId}
/>
) : null}
</div>
);
})}
</div>
</div>
</div>
)}
</div>
</div>

{threadKey ? (
<ErrorBoundary>
<CommunityThread community={community} channel={undefined} threadId={threadKey} />
</ErrorBoundary>
) : null}
</div>
</div>
</ErrorBoundary>
10 changes: 5 additions & 5 deletions packages/apps/owner-app/index.html
Original file line number Diff line number Diff line change
@@ -5,11 +5,11 @@
<base href="/owner/" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="shortcut icon" href="/icons/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png?v=202501" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png?v=202501" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png?v=202501" />
<link rel="manifest" href="/icons/site.webmanifest?v=202501" />
<link rel="shortcut icon" href="/icons/favicon.ico?v=202501" />
<meta name="theme-color" content="#ffffff" />

<title>Homebase | Owner console</title>
4 changes: 2 additions & 2 deletions packages/apps/owner-app/public/icons/site.webmanifest
Original file line number Diff line number Diff line change
@@ -5,12 +5,12 @@
"scope": "/",
"icons": [
{
"src": "/owner/icons/android-chrome-192x192.png",
"src": "/owner/icons/android-chrome-192x192.png?v=202501",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/owner/icons/android-chrome-512x512.png",
"src": "/owner/icons/android-chrome-512x512.png?v=202501",
"sizes": "512x512",
"type": "image/png"
}
Original file line number Diff line number Diff line change
@@ -88,7 +88,7 @@ export const ConfirmDialog = ({
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 dark:bg-slate-900 flex flex-col sm:flex-row-reverse px-6 gap-2">
<div className="bg-gray-50 py-3 dark:bg-slate-900 flex flex-col sm:flex-row-reverse px-6 gap-2">
<button
type="button"
className={`${!isValid ? 'pointer-events-none opacity-40' : ''} ${
Original file line number Diff line number Diff line change
@@ -208,7 +208,7 @@ export const useArticleComposer = ({
},
versionTag: (uploadResult as UploadResult).newVersionTag,
},
// We force set the keyHeader as it's returned from the server, and needed for fast saves afterwards
// We force set the keyHeader as it's returned from the upload, and needed for fast saves afterwards
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sharedSecretEncryptedKeyHeader: (uploadResult as UploadResult).keyHeader as any,
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { useState } from 'react';

import { cn, withRef } from '@udecode/cn';
import { getMentionOnSelectItem, TMentionItemBase } from '@udecode/plate-mention';
import {
BaseMentionPlugin,
MentionConfig,
MentionOnSelectItem,
TMentionItemBase,
} from '@udecode/plate-mention';

export interface Mentionable extends TMentionItemBase {
key: string;
key: string; // unique key
text: string; // text to insert and used as search value
value?: string; // (optional) value to insert
label?: string; // (optional) label to display
}

import {
@@ -14,11 +22,38 @@ import {
InlineComboboxInput,
InlineComboboxItem,
} from './inline-combobox';
import { AnyPluginConfig } from '@udecode/plate-core';
import { PlateElement } from './plate-element';
import { MentionPlugin } from '@udecode/plate-mention/react';
import {
moveSelection,
getBlockAbove,
isEndPoint,
insertText,
getEditorPlugin,
AnyPluginConfig,
} from '@udecode/plate-common';

const onSelectItem: MentionOnSelectItem<Mentionable> = (editor, item, search = '') => {
const { getOptions, tf } = getEditorPlugin<MentionConfig>(editor, {
key: BaseMentionPlugin.key,
});
const { insertSpaceAfterMention } = getOptions();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
tf.insert.mention({ key: item.value || item.key, search, value: item.value || item.text } as any);

// move the selection after the element
moveSelection(editor, { unit: 'offset' });

const pathAbove = getBlockAbove(editor)?.[1];

const isBlockEnd =
editor.selection && pathAbove && isEndPoint(editor, editor.selection.anchor, pathAbove);

const onSelectItem = getMentionOnSelectItem();
if (isBlockEnd && insertSpaceAfterMention) {
insertText(editor, ' ');
}
};

interface MentionOptions extends AnyPluginConfig {
mentionables?: Mentionable[];
@@ -42,7 +77,6 @@ export const MentionInputElement = withRef<typeof PlateElement>(({ className, ..
trigger="@"
value={search}
hideWhenSpace={true}
// hideWhenNoValue={true}
>
<span
className={cn(
@@ -58,11 +92,11 @@ export const MentionInputElement = withRef<typeof PlateElement>(({ className, ..

{mentionables.map((item) => (
<InlineComboboxItem
key={item.key}
key={item.value || item.key}
onClick={() => onSelectItem(editor, item, search)}
value={item.text.replaceAll('@', '')}
value={item.label || item.text}
>
{item.text}
{item.label || item.text}
</InlineComboboxItem>
))}
</InlineComboboxContent>
3 changes: 2 additions & 1 deletion packages/libs/js-lib/src/peer/peerData/PeerTypes.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import {
ScheduleOptions,
TransferUploadStatus,
} from '../../core/DriveData/Upload/DriveUploadTypes';
import { TargetDrive } from '../../core/core';
import { KeyHeader, TargetDrive } from '../../core/core';

export interface TransitQueryBatchRequest {
queryParams: FileQueryParams;
@@ -34,4 +34,5 @@ export interface TransitUploadResult {
globalTransitId: string;
targetDrive: TargetDrive;
};
keyHeader: KeyHeader | undefined;
}
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ export const uploadFileOverPeer = async (
aesKey?: Uint8Array | undefined;
axiosConfig?: AxiosRequestConfig;
}
) => {
): Promise<TransitUploadResult | void> => {
isDebug &&
console.debug(
'request',
@@ -95,7 +95,7 @@ export const uploadFileOverPeer = async (
},
};

const response = await client
const uploadResult = await client
.post<TransitUploadResult>(url, data, config)
.then((response) => {
const recipientStatus = response.data.recipientStatus;
@@ -116,10 +116,12 @@ export const uploadFileOverPeer = async (
console.debug(
'response',
new URL(`${dotYouClient.getEndpoint()}/transit/sender/files/send'`).pathname,
response
uploadResult
);

return response;
if (!uploadResult) return;
uploadResult.keyHeader = keyHeader;
return uploadResult;
};

export const uploadHeaderOverPeer = async (
Original file line number Diff line number Diff line change
@@ -213,7 +213,7 @@ const uploadPost = async <T extends PostContent>(
recipients: [odinId],
};

const result: TransitUploadResult = await uploadFileOverPeer(
const result = await uploadFileOverPeer(
dotYouClient,
transitInstructionSet,
metadata,

0 comments on commit fc4cc76

Please sign in to comment.