Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ def include_if_exists(path):
print("skipping %s because it does not exist", path)

include_if_exists('../daily-api/Tiltfile')
include_if_exists('../flyting/Tiltfile')
include_if_exists('../adhoc-infra/Tiltfile')
include_if_exists('../post-scraper-one-ai/Tiltfile')
include_if_exists('../njord/Tiltfile')
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"graphql-ws": "^5.5.5",
"jotai": "^2.12.2",
"lottie-react": "^2.4.1",
"mediasoup-client": "^3.7.13",
"node-emoji": "^2.2.0",
"node-fetch": "^2.6.6",
"react-hook-form": "7.54.2",
Expand Down
4 changes: 3 additions & 1 deletion packages/shared/src/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface MainLayoutProps
onNavTabClick?: (tab: string) => void;
canGoBack?: string;
hideBackButton?: boolean;
hideFeedbackWidget?: boolean;
}

export const feeds = Object.values(SharedFeedPage);
Expand All @@ -77,6 +78,7 @@ function MainLayoutComponent({
onLogoClick,
onNavTabClick,
canGoBack,
hideFeedbackWidget = false,
}: MainLayoutProps): ReactElement | null {
const router = useRouter();
const { logEvent } = useLogContext();
Expand Down Expand Up @@ -220,7 +222,7 @@ function MainLayoutComponent({
)}
{children}
</main>
<FeedbackWidget />
{!hideFeedbackWidget && <FeedbackWidget />}
</div>
);
}
Expand Down
145 changes: 106 additions & 39 deletions packages/shared/src/components/fields/EmojiPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactElement } from 'react';
import type { ReactElement, ReactNode } from 'react';
import React, {
useState,
useCallback,
Expand Down Expand Up @@ -44,37 +44,84 @@ const COMMON_EMOJIS = [
'❤️',
];

const DROPDOWN_GAP = 4;
const VIEWPORT_MARGIN = 12;
const MIN_DROPDOWN_WIDTH = 300;
const MIN_DROPDOWN_HEIGHT = 160;
const FALLBACK_DROPDOWN_HEIGHT = 320;

type EmojiPickerProps = {
value: string;
onChange: (emoji: string) => void;
label?: string;
label?: string | null;
className?: string;
renderTrigger?: (props: {
isOpen: boolean;
value: string;
toggleOpen: () => void;
clearValue: () => void;
}) => ReactNode;
};

export const EmojiPicker = ({
value,
onChange,
label = 'Icon (optional)',
className,
renderTrigger,
}: EmojiPickerProps): ReactElement => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [dropdownPosition, setDropdownPosition] = useState({
top: 0,
left: 0,
width: 0,
maxHeight: FALLBACK_DROPDOWN_HEIGHT,
});
const inputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);

const updateDropdownPosition = useCallback(() => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
const dropdownHeight =
dropdownRef.current?.offsetHeight ?? FALLBACK_DROPDOWN_HEIGHT;
const width = Math.min(
Math.max(rect.width, MIN_DROPDOWN_WIDTH),
window.innerWidth - VIEWPORT_MARGIN * 2,
);
const left = Math.min(
Math.max(rect.left, VIEWPORT_MARGIN),
window.innerWidth - width - VIEWPORT_MARGIN,
);
const availableBelow =
window.innerHeight - rect.bottom - VIEWPORT_MARGIN - DROPDOWN_GAP;
const availableAbove = rect.top - VIEWPORT_MARGIN - DROPDOWN_GAP;
const shouldOpenAbove =
availableBelow < dropdownHeight && availableAbove > availableBelow;
const maxHeight = Math.max(
shouldOpenAbove ? availableAbove : availableBelow,
MIN_DROPDOWN_HEIGHT,
);
const top = shouldOpenAbove
? Math.max(
VIEWPORT_MARGIN,
rect.top - Math.min(dropdownHeight, maxHeight) - DROPDOWN_GAP,
)
: Math.min(
rect.bottom + DROPDOWN_GAP,
window.innerHeight -
Math.min(dropdownHeight, maxHeight) -
VIEWPORT_MARGIN,
);

setDropdownPosition({
top: rect.bottom + 4,
left: rect.left,
width: rect.width,
top,
left,
width,
maxHeight,
});
}
}, []);
Expand All @@ -84,7 +131,7 @@ export const EmojiPicker = ({
updateDropdownPosition();
inputRef.current?.focus();
}
}, [isOpen, updateDropdownPosition]);
}, [isOpen, updateDropdownPosition, searchQuery]);

useEffect(() => {
if (!isOpen) {
Expand Down Expand Up @@ -125,6 +172,16 @@ export const EmojiPicker = ({
[onChange],
);

const clearValue = useCallback(() => {
onChange('');
setIsOpen(false);
setSearchQuery('');
}, [onChange]);

const toggleOpen = useCallback(() => {
setIsOpen((open) => !open);
}, []);

const searchResults = useMemo(() => {
if (!searchQuery.trim()) {
return [];
Expand All @@ -138,53 +195,63 @@ export const EmojiPicker = ({
return (
<div
ref={containerRef}
className={classNames('relative flex flex-col gap-2', className)}
className={classNames(
'relative flex flex-col',
label ? 'gap-2' : 'gap-0',
className,
)}
>
<Typography bold type={TypographyType.Callout}>
{label}
</Typography>
{label ? (
<Typography bold type={TypographyType.Callout}>
{label}
</Typography>
) : null}

<div ref={triggerRef} className="flex items-center gap-2">
{value && (
<Button
type="button"
variant={ButtonVariant.Float}
onClick={() => {
onChange('');
setIsOpen(false);
}}
className="!size-10 shrink-0"
>
-
</Button>
)}
<div ref={triggerRef}>
{renderTrigger ? (
renderTrigger({ isOpen, value, toggleOpen, clearValue })
) : (
<div className="flex items-center gap-2">
{value && (
<Button
type="button"
variant={ButtonVariant.Float}
onClick={clearValue}
className="!size-10 shrink-0"
>
-
</Button>
)}

{value && (
<div className="flex size-10 items-center justify-center rounded-10 border border-border-subtlest-tertiary bg-surface-float text-xl">
{value}
</div>
)}

{value && (
<div className="flex size-10 items-center justify-center rounded-10 border border-border-subtlest-tertiary bg-surface-float text-xl">
{value}
<Button
type="button"
variant={ButtonVariant.Float}
onClick={toggleOpen}
className="shrink-0"
>
{isOpen && 'Close'}
{!isOpen && value && 'Change'}
{!isOpen && !value && 'Pick emoji'}
</Button>
</div>
)}

<Button
type="button"
variant={ButtonVariant.Float}
onClick={() => setIsOpen(!isOpen)}
className="shrink-0"
>
{isOpen && 'Close'}
{!isOpen && value && 'Change'}
{!isOpen && !value && 'Pick emoji'}
</Button>
</div>

{isOpen && (
<div
ref={dropdownRef}
className="fixed z-[100] max-h-80 overflow-y-auto rounded-16 border border-border-subtlest-tertiary bg-background-default p-3 shadow-2"
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
minWidth: '300px',
maxHeight: `${dropdownPosition.maxHeight}px`,
}}
>
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface RichTextToolbarProps {
onLinkAdd: (url: string, label?: string) => void;
inlineActions?: ReactNode;
rightActions?: ReactNode;
allowBlockFormatting?: boolean;
}

export interface RichTextToolbarRef {
Expand Down Expand Up @@ -71,7 +72,13 @@ const ToolbarButton = ({
};

function RichTextToolbarComponent(
{ editor, onLinkAdd, inlineActions, rightActions }: RichTextToolbarProps,
{
editor,
onLinkAdd,
inlineActions,
rightActions,
allowBlockFormatting = true,
}: RichTextToolbarProps,
ref: Ref<RichTextToolbarRef>,
): ReactElement {
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
Expand Down Expand Up @@ -154,19 +161,23 @@ function RichTextToolbarComponent(
isActive={editorState.isItalic}
onClick={() => editor.chain().focus().toggleItalic().run()}
/>
<div className="mx-1 h-4 w-px bg-border-subtlest-tertiary" />
<ToolbarButton
tooltip="Bullet list (⌘⇧8)"
icon={<BulletListIcon />}
isActive={editorState.isBulletList}
onClick={() => editor.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
tooltip="Numbered list (⌘⇧7)"
icon={<NumberedListIcon />}
isActive={editorState.isOrderedList}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
/>
{allowBlockFormatting ? (
<>
<div className="mx-1 h-4 w-px bg-border-subtlest-tertiary" />
<ToolbarButton
tooltip="Bullet list (⌘⇧8)"
icon={<BulletListIcon />}
isActive={editorState.isBulletList}
onClick={() => editor.chain().focus().toggleBulletList().run()}
/>
<ToolbarButton
tooltip="Numbered list (⌘⇧7)"
icon={<NumberedListIcon />}
isActive={editorState.isOrderedList}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
/>
</>
) : null}
<div className="mx-1 h-4 w-px bg-border-subtlest-tertiary" />
<ToolbarButton
tooltip={editorState.isLink ? 'Edit link (⌘K)' : 'Add link (⌘K)'}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { Editor } from '@tiptap/react';
import type { UserShortProfile } from '../../../lib/user';
Expand All @@ -16,6 +16,7 @@ interface UseMentionAutocompleteProps {
sourceId?: string;
userId?: string;
onOffsetUpdate: (editor: Editor) => void;
suggestions?: UserShortProfile[];
}

export function useMentionAutocomplete({
Expand All @@ -24,12 +25,13 @@ export function useMentionAutocomplete({
sourceId,
userId,
onOffsetUpdate,
suggestions,
}: UseMentionAutocompleteProps) {
const { requestMethod } = useRequestProtocol();
const [query, setQuery] = useState<string>(undefined);
const [query, setQuery] = useState<string | undefined>(undefined);
const [selected, setSelected] = useState(0);
const mentionRangeRef = useRef<EditorRange>(null);
const queryRef = useRef<string>(undefined);
const queryRef = useRef<string | undefined>(undefined);
const selectedRef = useRef(0);
const mentionsRef = useRef<UserShortProfile[]>([]);

Expand All @@ -43,12 +45,40 @@ export function useMentionAutocomplete({
{ postId, query, sourceId },
{ requestKey: JSON.stringify(key) },
),
enabled: !!userId && typeof query !== 'undefined',
enabled: !suggestions && !!userId && typeof query !== 'undefined',
refetchOnWindowFocus: false,
refetchOnMount: false,
});

const mentions = data?.recommendedMentions;
const mentions = useMemo(() => {
if (!suggestions) {
return data?.recommendedMentions?.filter(
(mention) => mention.id !== userId,
);
}

if (typeof query === 'undefined') {
return [];
}

const normalizedQuery = query.trim().toLowerCase();
const filtered = suggestions.filter((suggestion) => {
if (suggestion.id === userId) {
return false;
}

if (!normalizedQuery) {
return true;
}

return (
suggestion.username.toLowerCase().startsWith(normalizedQuery) ||
suggestion.name?.toLowerCase().startsWith(normalizedQuery) === true
);
});

return filtered.slice(0, 8);
}, [data?.recommendedMentions, query, suggestions, userId]);

useEffect(() => {
queryRef.current = query;
Expand Down
Loading
Loading