diff --git a/packages/frontend/src/components/composer/EmojiAndStickerPicker.tsx b/packages/frontend/src/components/composer/EmojiAndStickerPicker.tsx index 89ebcdea9e..b2a2e7961f 100644 --- a/packages/frontend/src/components/composer/EmojiAndStickerPicker.tsx +++ b/packages/frontend/src/components/composer/EmojiAndStickerPicker.tsx @@ -3,6 +3,7 @@ import React, { useEffect, forwardRef, PropsWithChildren, + useRef, } from 'react' import classNames from 'classnames' @@ -17,6 +18,10 @@ import useMessage from '../../hooks/chat/useMessage' import styles from './styles.module.scss' import type { EmojiData } from 'emoji-mart/index' +import { + RovingTabindexProvider, + useRovingTabindex, +} from '../../contexts/RovingTabindex' type Props = { stickerPackName: string @@ -34,6 +39,8 @@ const DisplayedStickerPack = ({ const { jumpToMessage } = useMessage() const accountId = selectedAccountId() + const listRef = useRef(null) + const onClickSticker = (fileName: string) => { const stickerPath = fileName.replace('file://', '') BackendRemote.rpc @@ -47,21 +54,42 @@ const DisplayedStickerPack = ({ return (
{stickerPackName}
-
- {stickerPackImages.map((filePath, index) => ( - - ))} +
+ {/* Yes, we have separate `RovingTabindexProvider` for each + sticker pack, instead of having one for all stickers. + Users probably want to switch between sticker packs with Tab. */} + + {stickerPackImages.map((filePath, index) => ( + onClickSticker(filePath)} + /> + ))} +
) } +function StickersListItem(props: { filePath: string; onClick: () => void }) { + const { filePath, onClick } = props + const ref = useRef(null) + const rovingTabindex = useRovingTabindex(ref) + return ( + + ) +} + export const StickerPicker = ({ stickers, chatId, diff --git a/packages/frontend/src/components/dialogs/AddMember/AddMemberInnerDialog.tsx b/packages/frontend/src/components/dialogs/AddMember/AddMemberInnerDialog.tsx index 0e4ec9fd65..52b1a9b8d1 100644 --- a/packages/frontend/src/components/dialogs/AddMember/AddMemberInnerDialog.tsx +++ b/packages/frontend/src/components/dialogs/AddMember/AddMemberInnerDialog.tsx @@ -22,6 +22,7 @@ import InfiniteLoader from 'react-window-infinite-loader' import { AddMemberChip } from './AddMemberDialog' import styles from './styles.module.scss' import classNames from 'classnames' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' export function AddMemberInnerDialog({ onCancel, @@ -275,58 +276,61 @@ export function AddMemberInnerDialog({ // minimumBatchSize={100} > {({ onItemsRendered, ref }) => ( - // Not using 'react-window' results in ~5 second rendering time - // if the user has 5000 contacts. - // (see https://github.com/deltachat/deltachat-desktop/issues/1830) - { - const isExtraItem = index >= contactIds.length - return isExtraItem ? 'addContact' : contactIds[index] - }} - onItemsRendered={onItemsRendered} - ref={ref} - height={height} - width='100%' - // TODO fix: The size of each item is determined - // by `--local-avatar-size` and `--local-avatar-vertical-margin`, - // which might be different, e.g. currently they're smaller for - // "Rocket Theme", which results in gaps between the elements. - itemSize={64} - > - {({ index, style, data: contactIds }) => { - const isExtraItem = index >= contactIds.length - if (isExtraItem) { - return renderAddContact() - } + + {/* Not using 'react-window' results in ~5 second rendering time + if the user has 5000 contacts. + (see https://github.com/deltachat/deltachat-desktop/issues/1830) */} + { + const isExtraItem = index >= contactIds.length + return isExtraItem ? 'addContact' : contactIds[index] + }} + onItemsRendered={onItemsRendered} + ref={ref} + height={height} + width='100%' + // TODO fix: The size of each item is determined + // by `--local-avatar-size` and `--local-avatar-vertical-margin`, + // which might be different, e.g. currently they're smaller for + // "Rocket Theme", which results in gaps between the elements. + itemSize={64} + > + {({ index, style, data: contactIds }) => { + const isExtraItem = index >= contactIds.length + if (isExtraItem) { + return renderAddContact() + } - const contact = contactCache[contactIds[index]] - if (!contact) { - // Not loaded yet - return
- } + const contact = contactCache[contactIds[index]] + if (!contact) { + // Not loaded yet + return
+ } - return ( -
- c.id === contact.id) || - contactIdsInGroup.includes(contact.id) - } - disabled={ - contactIdsInGroup.includes(contact.id) || - contact.id === C.DC_CONTACT_ID_SELF - } - onCheckboxClick={toggleMember} - showRemove={false} - /> -
- ) - }} -
+ return ( +
+ c.id === contact.id + ) || contactIdsInGroup.includes(contact.id) + } + disabled={ + contactIdsInGroup.includes(contact.id) || + contact.id === C.DC_CONTACT_ID_SELF + } + onCheckboxClick={toggleMember} + showRemove={false} + /> +
+ ) + }} +
+ )} )} diff --git a/packages/frontend/src/components/dialogs/CreateChat/index.tsx b/packages/frontend/src/components/dialogs/CreateChat/index.tsx index 84263c5f22..adcb620e06 100644 --- a/packages/frontend/src/components/dialogs/CreateChat/index.tsx +++ b/packages/frontend/src/components/dialogs/CreateChat/index.tsx @@ -419,6 +419,8 @@ export function CreateGroup(props: CreateGroupProps) { const [errorMissingGroupName, setErrorMissingGroupName] = useState(false) const [groupContacts, setGroupContacts] = useState([]) + const groupMemberContactListWrapperRef = useRef(null) + useMemo(() => { BackendRemote.rpc .getContactsByIds(accountId, groupMembers) @@ -459,18 +461,25 @@ export function CreateGroup(props: CreateGroupProps) { quantity: groupMembers.length, })}
-
- - { - removeGroupMember(c) - }} - /> +
+ + + { + removeGroupMember(c) + }} + /> +
@@ -525,6 +534,8 @@ function CreateBroadcastList(props: CreateBroadcastListProps) { const [broadcastContacts, setBroadcastContacts] = useState([]) + const groupMemberContactListWrapperRef = useRef(null) + useMemo(() => { BackendRemote.rpc .getContactsByIds(accountId, broadcastRecipients) @@ -585,18 +596,25 @@ function CreateBroadcastList(props: CreateBroadcastListProps) { })}
)} -
- - { - removeBroadcastRecipient(c) - }} - /> +
+ + + { + removeBroadcastRecipient(c) + }} + /> +
diff --git a/packages/frontend/src/components/dialogs/ForwardMessage/index.tsx b/packages/frontend/src/components/dialogs/ForwardMessage/index.tsx index dac8f445a3..c2177a4a18 100644 --- a/packages/frontend/src/components/dialogs/ForwardMessage/index.tsx +++ b/packages/frontend/src/components/dialogs/ForwardMessage/index.tsx @@ -1,5 +1,5 @@ import AutoSizer from 'react-virtualized-auto-sizer' -import React, { useState } from 'react' +import React, { useRef, useState } from 'react' import { C } from '@deltachat/jsonrpc-client' import ChatListItem from '../../chat/ChatListItem' @@ -20,6 +20,7 @@ import styles from './styles.module.scss' import type { T } from '@deltachat/jsonrpc-client' import type { DialogProps } from '../../../contexts/DialogContext' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' type Props = { message: T.Message @@ -42,6 +43,8 @@ export default function ForwardMessage(props: Props) { const { isChatLoaded, loadChats, chatCache } = useLogicVirtualChatList(chatListIds) + const chatListRef = useRef(null) + const onChatClick = async (chatId: number) => { const chat = await BackendRemote.rpc.getFullChatById(accountId, chatId) onClose() @@ -96,37 +99,39 @@ export default function ForwardMessage(props: Props) { spellCheck={false} />
-
- {noResults && queryStr && ( - - )} -
- - {({ width, height }) => ( - 'key' + chatListIds[index]} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} - > - {({ index, style }) => { - const chatId = chatListIds[index] - return ( -
- -
- ) - }} -
- )} -
-
+
+ + {noResults && queryStr && ( + + )} +
+ + {({ width, height }) => ( + 'key' + chatListIds[index]} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {({ index, style }) => { + const chatId = chatListIds[index] + return ( +
+ +
+ ) + }} +
+ )} +
+
+
diff --git a/packages/frontend/src/components/dialogs/MailtoDialog/index.tsx b/packages/frontend/src/components/dialogs/MailtoDialog/index.tsx index 96c585c0db..bd1ac4c6ed 100644 --- a/packages/frontend/src/components/dialogs/MailtoDialog/index.tsx +++ b/packages/frontend/src/components/dialogs/MailtoDialog/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useRef, useState } from 'react' import AutoSizer from 'react-virtualized-auto-sizer' import { C } from '@deltachat/jsonrpc-client' @@ -15,6 +15,7 @@ import useTranslationFunction from '../../../hooks/useTranslationFunction' import styles from './styles.module.scss' import type { DialogProps } from '../../../contexts/DialogContext' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' type Props = { messageText: string @@ -32,6 +33,8 @@ export default function MailtoDialog(props: Props & DialogProps) { const { isChatLoaded, loadChats, chatCache } = useLogicVirtualChatList(chatListIds) + const resultsRef = useRef(null) + const onChatClick = async (chatId: number) => { createDraftMessage(accountId, chatId, messageText) onClose() @@ -65,32 +68,38 @@ export default function MailtoDialog(props: Props & DialogProps) { {noResults && queryStr && ( )} -
- - {({ width, height }) => ( - 'key' + chatListIds[index]} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} - > - {({ index, style }) => { - const chatId = chatListIds[index] - return ( -
- -
- ) - }} -
- )} -
+
+ + + {({ width, height }) => ( + 'key' + chatListIds[index]} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {({ index, style }) => { + const chatId = chatListIds[index] + return ( +
+ +
+ ) + }} +
+ )} +
+
diff --git a/packages/frontend/src/components/dialogs/ReactionsDialog/index.tsx b/packages/frontend/src/components/dialogs/ReactionsDialog/index.tsx index 88b468bcaf..8dc704c886 100644 --- a/packages/frontend/src/components/dialogs/ReactionsDialog/index.tsx +++ b/packages/frontend/src/components/dialogs/ReactionsDialog/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import classNames from 'classnames' import Dialog, { @@ -21,6 +21,10 @@ import type { DialogProps } from '../../../contexts/DialogContext' import { type T, C } from '@deltachat/jsonrpc-client' import useOpenViewProfileDialog from '../../../hooks/dialog/useOpenViewProfileDialog' +import { + RovingTabindexProvider, + useRovingTabindex, +} from '../../../contexts/RovingTabindex' export type Props = { reactionsByContact: T.Reactions['reactionsByContact'] @@ -62,6 +66,8 @@ function ReactionsDialogList({ reactionsByContact, onClose }: Props) { const [contacts, setContacts] = useState([]) const openViewProfileDialog = useOpenViewProfileDialog({ onAction: onClose }) + const ref = useRef(null) + useEffect(() => { const resolveContacts = async () => { const contactIds = Object.keys(reactionsByContact).map(contactId => @@ -90,35 +96,62 @@ function ReactionsDialogList({ reactionsByContact, onClose }: Props) { }, [accountId, reactionsByContact]) return ( -
    - {contacts.map(contact => { - const notFromSelf = C.DC_CONTACT_ID_SELF !== contact.id - return ( +
      + + {contacts.map(contact => (
    • - + + openViewProfileDialog(accountId, contactId) + } + />
    • - ) - })} + ))} +
    ) } + +function ReactionsDialogListItem(props: { + contact: ContactWithReaction + onClickNonSelf: (contactId: number) => void +}) { + const { contact, onClickNonSelf } = props + const notFromSelf = C.DC_CONTACT_ID_SELF !== contact.id + + const ref = useRef(null) + const rovingTabindex = useRovingTabindex(ref) + + return ( + + ) +} diff --git a/packages/frontend/src/components/dialogs/SelectContact/index.tsx b/packages/frontend/src/components/dialogs/SelectContact/index.tsx index 21ccda8942..4704d0c240 100644 --- a/packages/frontend/src/components/dialogs/SelectContact/index.tsx +++ b/packages/frontend/src/components/dialogs/SelectContact/index.tsx @@ -16,6 +16,7 @@ import { ContactListItem } from '../../contact/ContactListItem' import { FixedSizeList } from 'react-window' import AutoSizer from 'react-virtualized-auto-sizer' import InfiniteLoader from 'react-window-infinite-loader' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' /** * display a dialog with a react-window of contacts @@ -39,6 +40,8 @@ export default function SelectContactDialog({ ) const tx = useTranslationFunction() + const selectContactListRef = useRef(null) + const infiniteLoaderRef = useRef(null) // By default InfiniteLoader assumes that each item's index in the list // never changes. But in our case they do change because of filtering. @@ -66,7 +69,7 @@ export default function SelectContactDialog({ /> -
    +
    {({ height }) => ( {({ onItemsRendered, ref }) => ( - contactIds[index]} - onItemsRendered={onItemsRendered} - ref={ref} - height={height} - width='100%' - itemSize={64} + - {({ index, style }) => { - const el = (() => { - const item = contactCache[contactIds[index]] - if (!item) { - // It's not loaded yet - return null - } - const contact: T.Contact = item - return ( - - ) - })() + contactIds[index]} + onItemsRendered={onItemsRendered} + ref={ref} + height={height} + width='100%' + itemSize={64} + > + {({ index, style }) => { + const el = (() => { + const item = contactCache[contactIds[index]] + if (!item) { + // It's not loaded yet + return null + } + const contact: T.Contact = item + return ( + + ) + })() - return
    {el}
    - }} -
    + return
    {el}
    + }} +
    + )}
    )} diff --git a/packages/frontend/src/components/dialogs/UnblockContacts.tsx b/packages/frontend/src/components/dialogs/UnblockContacts.tsx index e88f3fbcfc..7d53f3d9d0 100644 --- a/packages/frontend/src/components/dialogs/UnblockContacts.tsx +++ b/packages/frontend/src/components/dialogs/UnblockContacts.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import debounce from 'debounce' import { ContactList } from '../contact/ContactList' @@ -9,6 +9,7 @@ import useConfirmationDialog from '../../hooks/dialog/useConfirmationDialog' import useTranslationFunction from '../../hooks/useTranslationFunction' import type { DialogProps } from '../../contexts/DialogContext' +import { RovingTabindexProvider } from '../../contexts/RovingTabindex' export default function UnblockContacts({ onClose }: DialogProps) { const [blockedContacts, setBlockedContacts] = useState( @@ -41,6 +42,8 @@ export default function UnblockContacts({ onClose }: DialogProps) { } } + const wrapperRef = useRef(null) + if (blockedContacts === null) return null return ( {tx('blocked_empty_hint')}

    } {blockedContacts.length > 0 && (
    - + + +
    )} diff --git a/packages/frontend/src/components/dialogs/ViewGroup.tsx b/packages/frontend/src/components/dialogs/ViewGroup.tsx index f13188b156..956cd2b473 100644 --- a/packages/frontend/src/components/dialogs/ViewGroup.tsx +++ b/packages/frontend/src/components/dialogs/ViewGroup.tsx @@ -36,6 +36,7 @@ import type { DialogProps } from '../../contexts/DialogContext' import ImageCropper from '../ImageCropper' import { AddMemberDialog } from './AddMember/AddMemberDialog' import AutoSizer from 'react-virtualized-auto-sizer' +import { RovingTabindexProvider } from '../../contexts/RovingTabindex' export default function ViewGroup( props: { @@ -121,6 +122,9 @@ function ViewGroupInner( const chatDisabled = !chat.canSend + const groupMemberContactListWrapperRef = useRef(null) + const relatedChatsListWrapperRef = useRef(null) + const { group, groupName, @@ -233,34 +237,41 @@ function ViewGroupInner( {isRelatedChatsEnabled && ( <>
    {tx('related_chats')}
    -
    - - {({ width }) => ( - 'key' + chatListIds[index]} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} - > - {({ index, style }) => { - const chatId = chatListIds[index] - return ( -
    - -
    - ) - }} -
    - )} -
    +
    + + + {({ width }) => ( + 'key' + chatListIds[index]} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {({ index, style }) => { + const chatId = chatListIds[index] + return ( +
    + +
    + ) + }} +
    + )} +
    +
    )} @@ -273,29 +284,38 @@ function ViewGroupInner( quantity: groupMembers.length, })}
    -
    - {!chatDisabled && ( - <> - showAddMemberDialog()} - isBroadcast={isBroadcast} - /> - {!isBroadcast && ( - showQRDialog()} /> - )} - - )} - { - if (contact.id === C.DC_CONTACT_ID_SELF) { - return - } - setProfileContact(contact) - }} - onRemoveClick={showRemoveGroupMemberConfirmationDialog} - /> +
    + + {!chatDisabled && ( + <> + showAddMemberDialog()} + isBroadcast={isBroadcast} + /> + {!isBroadcast && ( + showQRDialog()} + /> + )} + + )} + { + if (contact.id === C.DC_CONTACT_ID_SELF) { + return + } + setProfileContact(contact) + }} + onRemoveClick={showRemoveGroupMemberConfirmationDialog} + /> +
    diff --git a/packages/frontend/src/components/dialogs/ViewProfile/index.tsx b/packages/frontend/src/components/dialogs/ViewProfile/index.tsx index 7d2e6507e7..9a9350f31e 100644 --- a/packages/frontend/src/components/dialogs/ViewProfile/index.tsx +++ b/packages/frontend/src/components/dialogs/ViewProfile/index.tsx @@ -1,5 +1,5 @@ import AutoSizer from 'react-virtualized-auto-sizer' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import moment from 'moment' import { C } from '@deltachat/jsonrpc-client' @@ -28,6 +28,7 @@ import styles from './styles.module.scss' import type { DialogProps } from '../../../contexts/DialogContext' import type { T } from '@deltachat/jsonrpc-client' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' const log = getLogger('renderer/dialogs/ViewProfile') @@ -138,6 +139,8 @@ export function ViewProfileInner({ action?: () => void }>(null) + const mutualChatsListRef = useRef(null) + const isDeviceChat = contact.id === C.DC_CONTACT_ID_DEVICE const isSelfChat = contact.id === C.DC_CONTACT_ID_SELF @@ -313,34 +316,37 @@ export function ViewProfileInner({ <>
    {tx('profile_shared_chats')}
    - - {({ width, height }) => ( - 'key' + chatListIds[index]} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} - > - {({ index, style }) => { - const chatId = chatListIds[index] - return ( -
    - -
    - ) - }} -
    - )} -
    + + + {({ width, height }) => ( + 'key' + chatListIds[index]} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {({ index, style }) => { + const chatId = chatListIds[index] + return ( +
    + +
    + ) + }} +
    + )} +
    +
    )} diff --git a/packages/frontend/src/components/dialogs/WebxdcSendToChat/index.tsx b/packages/frontend/src/components/dialogs/WebxdcSendToChat/index.tsx index 3cd941bf8b..a2f2c58b75 100644 --- a/packages/frontend/src/components/dialogs/WebxdcSendToChat/index.tsx +++ b/packages/frontend/src/components/dialogs/WebxdcSendToChat/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react' +import React, { useCallback, useRef, useState } from 'react' import { C } from '@deltachat/jsonrpc-client' import AutoSizer from 'react-virtualized-auto-sizer' import classNames from 'classnames' @@ -26,6 +26,7 @@ import useTranslationFunction from '../../../hooks/useTranslationFunction' import styles from './styles.module.scss' import type { DialogProps } from '../../../contexts/DialogContext' +import { RovingTabindexProvider } from '../../../contexts/RovingTabindex' type Props = { messageText: string | null @@ -45,6 +46,8 @@ export default function WebxdcSaveToChatDialog(props: Props) { const { isChatLoaded, loadChats, chatCache } = useLogicVirtualChatList(chatListIds) + const resultsRef = useRef(null) + const onChatClick = async (chatId: number) => { let path = null if (file) { @@ -103,32 +106,38 @@ export default function WebxdcSaveToChatDialog(props: Props) { {noResults && queryStr && ( )} -
    - - {({ width, height }) => ( - 'key' + chatListIds[index]} - itemHeight={CHATLISTITEM_CHAT_HEIGHT} - > - {({ index, style }) => { - const chatId = chatListIds[index] - return ( -
    - -
    - ) - }} -
    - )} -
    +
    + + + {({ width, height }) => ( + 'key' + chatListIds[index]} + itemHeight={CHATLISTITEM_CHAT_HEIGHT} + > + {({ index, style }) => { + const chatId = chatListIds[index] + return ( +
    + +
    + ) + }} +
    + )} +
    +