mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-11 17:50:29 +03:00
Merge branch 'dev' into dev
This commit is contained in:
commit
bca5ac5cba
6 changed files with 174 additions and 47 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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={() => (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue