Merge branch 'dev' into dev

This commit is contained in:
Ginger 2025-02-19 10:36:09 -05:00 committed by GitHub
commit bca5ac5cba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 174 additions and 47 deletions

View file

@ -64,9 +64,7 @@ export function EmoticonAutocomplete({
}, [imagePacks]); }, [imagePacks]);
const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS); const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
const autoCompleteEmoticon = (result ? result.items : recentEmoji).sort((a, b) => const autoCompleteEmoticon = result ? result.items : recentEmoji;
a.shortcode.localeCompare(b.shortcode)
);
useEffect(() => { useEffect(() => {
if (query.text) search(query.text); if (query.text) search(query.text);

View file

@ -471,36 +471,34 @@ export function SearchEmojiGroup({
return ( return (
<EmojiGroup key={id} id={id} label={label}> <EmojiGroup key={id} id={id} label={label}>
{tab === EmojiBoardTab.Emoji {tab === EmojiBoardTab.Emoji
? searchResult ? searchResult.map((emoji) =>
.sort((a, b) => a.shortcode.localeCompare(b.shortcode)) 'unicode' in emoji ? (
.map((emoji) => <EmojiItem
'unicode' in emoji ? ( key={emoji.unicode}
<EmojiItem label={emoji.label}
key={emoji.unicode} type={EmojiType.Emoji}
label={emoji.label} data={emoji.unicode}
type={EmojiType.Emoji} shortcode={emoji.shortcode}
data={emoji.unicode} >
shortcode={emoji.shortcode} {emoji.unicode}
> </EmojiItem>
{emoji.unicode} ) : (
</EmojiItem> <EmojiItem
) : ( key={emoji.shortcode}
<EmojiItem label={emoji.body || emoji.shortcode}
key={emoji.shortcode} type={EmojiType.CustomEmoji}
label={emoji.body || emoji.shortcode} data={emoji.url}
type={EmojiType.CustomEmoji} shortcode={emoji.shortcode}
data={emoji.url} >
shortcode={emoji.shortcode} <img
> loading="lazy"
<img className={css.CustomEmojiImg}
loading="lazy" alt={emoji.body || emoji.shortcode}
className={css.CustomEmojiImg} src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
alt={emoji.body || emoji.shortcode} />
src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url} </EmojiItem>
/>
</EmojiItem>
)
) )
)
: searchResult.map((emoji) => : searchResult.map((emoji) =>
'unicode' in emoji ? null : ( 'unicode' in emoji ? null : (
<StickerItem <StickerItem

View file

@ -182,7 +182,9 @@ export function RoomNavItem({
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom); const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const typingMember = useRoomTypingMember(room.roomId); const typingMember = useRoomTypingMember(room.roomId).filter(
(receipt) => receipt.userId !== mx.getUserId()
);
const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => { const handleContextMenu: MouseEventHandler<HTMLElement> = (evt) => {
evt.preventDefault(); evt.preventDefault();
@ -219,7 +221,9 @@ export function RoomNavItem({
<RoomAvatar <RoomAvatar
roomId={room.roomId} roomId={room.roomId}
src={ src={
direct ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication) direct
? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
: getRoomAvatarUrl(mx, room, 96, useAuthentication)
} }
alt={room.name} alt={room.name}
renderFallback={() => ( renderFallback={() => (

View file

@ -79,6 +79,28 @@ function GlobalPackSelector({
}); });
}; };
const addSelected = (adds: PackAddress[]) => {
setSelected((addresses) => {
const newAddresses = Array.from(addresses);
adds.forEach((address) => {
if (newAddresses.find((addr) => packAddressEqual(addr, address))) {
return;
}
newAddresses.push(address);
});
return newAddresses;
});
};
const removeSelected = (adds: PackAddress[]) => {
setSelected((addresses) => {
const newAddresses = addresses.filter(
(addr) => !adds.find((address) => packAddressEqual(addr, address))
);
return newAddresses;
});
};
const hasSelected = selected.length > 0; const hasSelected = selected.length > 0;
return ( return (
<Box grow="Yes" direction="Column"> <Box grow="Yes" direction="Column">
@ -115,9 +137,35 @@ function GlobalPackSelector({
{Array.from(roomToPacks.entries()).map(([roomId, roomPacks]) => { {Array.from(roomToPacks.entries()).map(([roomId, roomPacks]) => {
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
if (!room) return null; if (!room) return null;
const roomPackAddresses = roomPacks
.map((pack) => pack.address)
.filter((addr) => addr !== undefined);
const allSelected = roomPackAddresses.every((addr) =>
selected.find((address) => packAddressEqual(addr, address))
);
return ( return (
<Box key={roomId} direction="Column" gap="100"> <Box key={roomId} direction="Column" gap="100">
<Text size="L400">{room.name}</Text> <Box alignItems="Center">
<Box grow="Yes">
<Text size="L400">{room.name}</Text>
</Box>
<Box shrink="No">
<Chip
variant={allSelected ? 'Critical' : 'Surface'}
radii="Pill"
onClick={() => {
if (allSelected) {
removeSelected(roomPackAddresses);
return;
}
addSelected(roomPackAddresses);
}}
>
<Text size="B300">{allSelected ? 'Unselect All' : 'Select All'}</Text>
</Chip>
</Box>
</Box>
{roomPacks.map((pack) => { {roomPacks.map((pack) => {
const avatarMxc = pack.getAvatarUrl(ImageUsage.Emoticon); const avatarMxc = pack.getAvatarUrl(ImageUsage.Emoticon);
const avatarUrl = avatarMxc const avatarUrl = avatarMxc
@ -126,7 +174,7 @@ function GlobalPackSelector({
const { address } = pack; const { address } = pack;
if (!address) return null; if (!address) return null;
const added = selected.find((addr) => packAddressEqual(addr, address)); const added = !!selected.find((addr) => packAddressEqual(addr, address));
return ( return (
<SequenceCard <SequenceCard
key={pack.id} key={pack.id}
@ -152,7 +200,11 @@ function GlobalPackSelector({
</Box> </Box>
} }
after={ after={
<Checkbox variant="Success" onClick={() => toggleSelect(address)} /> <Checkbox
checked={added}
variant="Success"
onClick={() => toggleSelect(address)}
/>
} }
/> />
</SequenceCard> </SequenceCard>

View file

@ -28,6 +28,81 @@ export type UseAsyncSearchResult<TSearchItem extends object | string | number> =
export type SearchResetHandler = () => void; export type SearchResetHandler = () => void;
const performMatch = (
target: string | string[],
query: string,
options?: UseAsyncSearchOptions
): string | undefined => {
if (Array.isArray(target)) {
const matchTarget = target.find((i) =>
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
);
return matchTarget ? normalize(matchTarget, options?.normalizeOptions) : undefined;
}
const normalizedTargetStr = normalize(target, options?.normalizeOptions);
const matches = matchQuery(normalizedTargetStr, query, options?.matchOptions);
return matches ? normalizedTargetStr : undefined;
};
export const orderSearchItems = <TSearchItem extends object | string | number>(
query: string,
items: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>,
options?: UseAsyncSearchOptions
): TSearchItem[] => {
const orderedItems: TSearchItem[] = Array.from(items);
// we will consider "_" as word boundary char.
// because in more use-cases it is used. (like: emojishortcode)
const boundaryRegex = new RegExp(`(\\b|_)${query}`);
const perfectBoundaryRegex = new RegExp(`(\\b|_)${query}(\\b|_)`);
orderedItems.sort((i1, i2) => {
const str1 = performMatch(getItemStr(i1, query), query, options);
const str2 = performMatch(getItemStr(i2, query), query, options);
if (str1 === undefined && str2 === undefined) return 0;
if (str1 === undefined) return 1;
if (str2 === undefined) return -1;
let points1 = 0;
let points2 = 0;
// short string should score more
const pointsToSmallStr = (points: number) => {
if (str1.length < str2.length) points1 += points;
else if (str2.length < str1.length) points2 += points;
};
pointsToSmallStr(1);
// closes query match should score more
const indexIn1 = str1.indexOf(query);
const indexIn2 = str2.indexOf(query);
if (indexIn1 < indexIn2) points1 += 2;
else if (indexIn2 < indexIn1) points2 += 2;
else pointsToSmallStr(2);
// query match word start on boundary should score more
const boundaryIn1 = str1.match(boundaryRegex);
const boundaryIn2 = str2.match(boundaryRegex);
if (boundaryIn1 && boundaryIn2) pointsToSmallStr(4);
else if (boundaryIn1) points1 += 4;
else if (boundaryIn2) points2 += 4;
// query match word start and end on boundary should score more
const perfectBoundaryIn1 = str1.match(perfectBoundaryRegex);
const perfectBoundaryIn2 = str2.match(perfectBoundaryRegex);
if (perfectBoundaryIn1 && perfectBoundaryIn2) pointsToSmallStr(8);
else if (perfectBoundaryIn1) points1 += 8;
else if (perfectBoundaryIn2) points2 += 8;
return points2 - points1;
});
return orderedItems;
};
export const useAsyncSearch = <TSearchItem extends object | string | number>( export const useAsyncSearch = <TSearchItem extends object | string | number>(
list: TSearchItem[], list: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>, getItemStr: SearchItemStrGetter<TSearchItem>,
@ -40,21 +115,15 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(
const handleMatch: MatchHandler<TSearchItem> = (item, query) => { const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
const itemStr = getItemStr(item, query); const itemStr = getItemStr(item, query);
if (Array.isArray(itemStr))
return !!itemStr.find((i) => const strWithMatch = performMatch(itemStr, query, options);
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions) return typeof strWithMatch === 'string';
);
return matchQuery(
normalize(itemStr, options?.normalizeOptions),
query,
options?.matchOptions
);
}; };
const handleResult: ResultHandler<TSearchItem> = (results, query) => const handleResult: ResultHandler<TSearchItem> = (results, query) =>
setResult({ setResult({
query, query,
items: [...results], items: orderSearchItems(query, results, getItemStr, options),
}); });
return AsyncSearch(list, handleMatch, handleResult, options); return AsyncSearch(list, handleMatch, handleResult, options);

View file

@ -427,6 +427,12 @@ a {
text-decoration: underline; text-decoration: underline;
} }
} }
[data-mx-spoiler][aria-pressed='true'] a {
color: transparent;
pointer-events: none;
}
b { b {
font-weight: var(--fw-medium); font-weight: var(--fw-medium);
} }