diff --git a/CHANGELOG.md b/CHANGELOG.md
index a716d95f1b..ee716339cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
- Prefer light theme for the help and webxdc loading pages
- Helper method to easily use confirmation dialogs #3601
- Refactor using new `useConfirmationDialog` hook #3602
+- Add multi selecting messages for forwarding and deleting #3602
diff --git a/scss/manifest.scss b/scss/manifest.scss
index 3620754b73..d65111c5cf 100644
--- a/scss/manifest.scss
+++ b/scss/manifest.scss
@@ -64,6 +64,7 @@
@import 'message/_status-icon';
@import 'message/_message-calls';
@import 'message/_message-markdown';
+@import 'message/_selected-messages-action.scss';
// misc
@import 'misc/_avatar.scss';
diff --git a/scss/message/_selected-messages-action.scss b/scss/message/_selected-messages-action.scss
new file mode 100644
index 0000000000..3a42f5b5d9
--- /dev/null
+++ b/scss/message/_selected-messages-action.scss
@@ -0,0 +1,16 @@
+.selected-messages-action {
+ display: flex;
+ flex-direction: row;
+ background-color: var(--navBarBackground);
+ justify-content: flex-start;
+ align-items: center;
+ & > p {
+ width: 100%;
+ text-align: right;
+ margin: 8px;
+ }
+ & > button {
+ margin: 4px;
+ margin-bottom: 6px !important;
+ }
+}
diff --git a/scss/misc/_delta-buttons.scss b/scss/misc/_delta-buttons.scss
index 5206ab933b..d5f7f299d7 100644
--- a/scss/misc/_delta-buttons.scss
+++ b/scss/misc/_delta-buttons.scss
@@ -16,7 +16,7 @@ button.delta-button-round {
border-color: rgba(206, 217, 224, 0.5);
background-image: none;
cursor: not-allowed;
- color: rgba(0, 0, 0, 0.2);
+ color: var(--globalText);
&:hover {
background-color: rgba(206, 217, 224, 0.5);
}
@@ -35,6 +35,56 @@ button.delta-button-round {
}
}
+button.delta-button {
+ color: var(--colorNone);
+ background-color: rgba(0, 0, 0, 0);
+ padding: 0 2px;
+ margin-bottom: 0px;
+ letter-spacing: 0px;
+ font-size: initial;
+ font-weight: 'initial';
+ text-align: center;
+ text-transform: uppercase;
+ padding: 5px;
+ border-style: solid;
+ border-color: transparent;
+ border-radius: 2px;
+ &:hover {
+ cursor: pointer;
+ background-color: #f1f1f1;
+ }
+
+ &.no-padding {
+ padding: 0px;
+ }
+
+ &.bold {
+ letter-spacing: 2px;
+ font-weight: bold;
+ }
+
+ &.light-bold {
+ letter-spacing: 1px;
+ font-weight: 100;
+ }
+
+ &.primary {
+ color: var(--colorPrimary);
+ }
+
+ &.danger {
+ color: var(--colorDanger);
+ }
+
+ &:disabled {
+ color: grey;
+ cursor: default;
+ &:hover {
+ cursor: default;
+ background-color: none;
+ }
+ }
+}
button.delta-button-tab {
padding: 8px;
margin: 5px;
diff --git a/src/renderer/components/attachment/messageAttachment.tsx b/src/renderer/components/attachment/messageAttachment.tsx
index 3802c94236..700a96cb07 100644
--- a/src/renderer/components/attachment/messageAttachment.tsx
+++ b/src/renderer/components/attachment/messageAttachment.tsx
@@ -27,6 +27,7 @@ type AttachmentProps = {
conversationType: ConversationType
message: Type.Message
hasQuote: boolean
+ isSelectMode: boolean
}
export default function Attachment({
@@ -34,6 +35,7 @@ export default function Attachment({
conversationType,
message,
hasQuote,
+ isSelectMode,
}: AttachmentProps) {
const tx = useTranslationFunction()
const { openDialog } = useDialog()
@@ -42,6 +44,7 @@ export default function Attachment({
}
const direction = getDirection(message)
const onClickAttachment = (ev: any) => {
+ if (isSelectMode) return
if (message.viewType === 'Sticker') return
ev.stopPropagation()
if (isDisplayableByFullscreenMedia(message.fileMime)) {
diff --git a/src/renderer/components/composer/EmojiAndStickerPicker.tsx b/src/renderer/components/composer/EmojiAndStickerPicker.tsx
index c3caac06da..d9f163ef34 100644
--- a/src/renderer/components/composer/EmojiAndStickerPicker.tsx
+++ b/src/renderer/components/composer/EmojiAndStickerPicker.tsx
@@ -16,6 +16,7 @@ import { useThemeCssVar } from '../../ThemeManager'
import type { EmojiData } from 'emoji-mart/index'
import useTranslationFunction from '../../hooks/useTranslationFunction'
+import Button from '../ui/Button'
const DisplayedStickerPack = ({
stickerPackName,
@@ -89,24 +90,18 @@ export const StickerPicker = ({
))}
-
>
) : (
{tx('add_stickers_instructions')}
-
+
{tx('open_sticker_folder')}
-
+
)}
diff --git a/src/renderer/components/dialogs/About.tsx b/src/renderer/components/dialogs/About.tsx
index 14033d58fc..3c30ee5972 100644
--- a/src/renderer/components/dialogs/About.tsx
+++ b/src/renderer/components/dialogs/About.tsx
@@ -11,6 +11,7 @@ import { DialogBody, DialogContent, DialogWithHeader } from '../Dialog'
import useTranslationFunction from '../../hooks/useTranslationFunction'
import type { DialogProps } from '../../contexts/DialogContext'
+import Button from '../ui/Button'
const log = getLogger('renderer/dialogs/About')
@@ -56,9 +57,9 @@ export function DCInfo(_props: any) {
-
+
{tx('copy_json')}
-
+
>
)
diff --git a/src/renderer/components/dialogs/ConfirmSendingFiles.tsx b/src/renderer/components/dialogs/ConfirmSendingFiles.tsx
index 99cc955d83..56d184303c 100644
--- a/src/renderer/components/dialogs/ConfirmSendingFiles.tsx
+++ b/src/renderer/components/dialogs/ConfirmSendingFiles.tsx
@@ -8,6 +8,7 @@ import {
} from './DeltaDialog'
import type { DialogProps } from '../../contexts/DialogContext'
+import Button from '../ui/Button'
type Props = {
onClick: (isConfirmed: boolean) => void
@@ -56,12 +57,12 @@ export default function ConfirmSendingFiles({
-
+
{tx('cancel')}
-
-
+
+
{tx('menu_send')}
-
+
diff --git a/src/renderer/components/dialogs/DeltaDialog.tsx b/src/renderer/components/dialogs/DeltaDialog.tsx
index f2f91c5a62..e3d730245e 100644
--- a/src/renderer/components/dialogs/DeltaDialog.tsx
+++ b/src/renderer/components/dialogs/DeltaDialog.tsx
@@ -3,6 +3,7 @@ import { Dialog, Classes, RadioGroup, Radio } from '@blueprintjs/core'
import classNames from 'classnames'
import type { DialogProps } from '../../contexts/DialogContext'
+import Button from '../ui/Button'
export const DeltaDialogBase = React.memo<
React.PropsWithChildren<
@@ -311,18 +312,18 @@ export function SmallSelectDialog({
- {
onCancel && onCancel()
onClose()
}}
>
{tx('cancel')}
-
-
+
+
{tx('save_desktop')}
-
+
@@ -368,25 +369,19 @@ export function DeltaDialogOkCancelFooter({
return (
-
- {cancelLabel}
-
-
+
+ {cancelLabel}
+
+
+
{confirmLabel}
-
+
)
@@ -398,9 +393,9 @@ export function DeltaDialogCloseFooter({ onClose }: { onClose: () => any }) {
return (
-
+
{tx('close')}
-
+
)
diff --git a/src/renderer/components/dialogs/EditProfileDialog/ProfileImageSelector.tsx b/src/renderer/components/dialogs/EditProfileDialog/ProfileImageSelector.tsx
index 6bafc1f51b..1f38887043 100644
--- a/src/renderer/components/dialogs/EditProfileDialog/ProfileImageSelector.tsx
+++ b/src/renderer/components/dialogs/EditProfileDialog/ProfileImageSelector.tsx
@@ -3,6 +3,7 @@ import React from 'react'
import { runtime } from '../../../runtime'
import { avatarInitial } from '../../Avatar'
import useTranslationFunction from '../../../hooks/useTranslationFunction'
+import Button from '../../ui/Button'
export default function ProfileImageSelector({
displayName,
@@ -48,21 +49,21 @@ export default function ProfileImageSelector({
{initial}
)}
<>
-
{tx('profile_image_select')}
-
-
+
{tx('profile_image_delete')}
-
+
>
)
diff --git a/src/renderer/components/dialogs/EditVideochatInstanceDialog.tsx b/src/renderer/components/dialogs/EditVideochatInstanceDialog.tsx
index 89f77a3e78..0a341a5e8b 100644
--- a/src/renderer/components/dialogs/EditVideochatInstanceDialog.tsx
+++ b/src/renderer/components/dialogs/EditVideochatInstanceDialog.tsx
@@ -2,8 +2,8 @@ import React, { useState } from 'react'
import { DeltaInput } from '../Login-Styles'
import { SettingsStoreState } from '../../stores/settings'
-import RadioGroup from '../RadioGroup'
-import Radio from '../Radio'
+import RadioGroup from '../ui/RadioGroup'
+import Radio from '../ui/Radio'
import {
VIDEO_CHAT_INSTANCE_AUTISTICI,
VIDEO_CHAT_INSTANCE_SYSTEMLI,
diff --git a/src/renderer/components/dialogs/ForwardMessage/index.tsx b/src/renderer/components/dialogs/ForwardMessage/index.tsx
index 0b3cea9920..3d07e8b07a 100644
--- a/src/renderer/components/dialogs/ForwardMessage/index.tsx
+++ b/src/renderer/components/dialogs/ForwardMessage/index.tsx
@@ -22,14 +22,15 @@ import styles from './styles.module.scss'
const LIST_FLAGS = C.DC_GCL_FOR_FORWARDING | C.DC_GCL_NO_SPECIALS
export default function ForwardMessage(props: {
- message: T.Message
+ message: T.Message | number[]
onClose: DialogProps['onClose']
+ onForward?: () => void
}) {
const accountId = selectedAccountId()
const tx = useTranslationFunction()
const { openDialog } = useDialog()
- const { message, onClose } = props
+ const { message, onClose, onForward } = props
const { chatListIds, queryStr, setQueryStr } = useChatList(LIST_FLAGS)
const { isChatLoaded, loadChats, chatCache } = useLogicVirtualChatList(
chatListIds,
@@ -38,6 +39,7 @@ export default function ForwardMessage(props: {
const onChatClick = async (chatId: number) => {
const chat = await BackendRemote.rpc.getFullChatById(accountId, chatId)
+ const isMany = Array.isArray(message)
onClose()
if (!chat.isSelfTalk) {
selectChat(chat.id)
@@ -48,10 +50,30 @@ export default function ForwardMessage(props: {
chat
)
if (!yes) {
- selectChat(message.chatId)
+ if (isMany) {
+ const message_ = await BackendRemote.rpc.getMessage(
+ accountId,
+ message[0]
+ )
+ selectChat(message_.chatId)
+ } else {
+ selectChat(message.chatId)
+ }
}
} else {
- await forwardMessage(accountId, message.id, chat.id)
+ if (isMany) {
+ const messageObjects: T.Message[] = await Promise.all(
+ message.map(id => BackendRemote.rpc.getMessage(accountId, id))
+ )
+ messageObjects.sort((msgA, msgB) => msgA.timestamp - msgB.timestamp)
+ const messageIds = messageObjects.map(msg => msg.id)
+ for (const messageId of messageIds) {
+ await forwardMessage(accountId, messageId, chat.id)
+ }
+ } else {
+ await forwardMessage(accountId, message.id, chat.id)
+ }
+ onForward && onForward()
}
}
const onSearchChange = (e: React.ChangeEvent) =>
diff --git a/src/renderer/components/dialogs/ViewGroup.tsx b/src/renderer/components/dialogs/ViewGroup.tsx
index e3187c7597..0fdb652501 100644
--- a/src/renderer/components/dialogs/ViewGroup.tsx
+++ b/src/renderer/components/dialogs/ViewGroup.tsx
@@ -37,6 +37,7 @@ import useTranslationFunction from '../../hooks/useTranslationFunction'
import useConfirmationDialog from '../../hooks/useConfirmationDialog'
import type { DialogProps } from '../../contexts/DialogContext'
+import Button from '../ui/Button'
const log = getLogger('renderer/ViewGroup')
@@ -528,21 +529,21 @@ export function GroupImageSelector({
{initial}
)}
<>
-
{tx('change_group_image')}
-
-
+
{tx('remove_group_image')}
-
+
>
)
diff --git a/src/renderer/components/dialogs/ViewProfile/index.tsx b/src/renderer/components/dialogs/ViewProfile/index.tsx
index 7e7d572d7c..b88b2d921e 100644
--- a/src/renderer/components/dialogs/ViewProfile/index.tsx
+++ b/src/renderer/components/dialogs/ViewProfile/index.tsx
@@ -28,6 +28,7 @@ import { MessagesDisplayContext } from '../../../contexts/MessagesDisplayContext
import styles from './styles.module.scss'
import type { DialogProps } from '../../../contexts/DialogContext'
+import Button from '../../ui/Button'
const log = getLogger('renderer/dialogs/ViewProfile')
@@ -275,14 +276,15 @@ export function ViewProfileInner({
}}
>
{!isDeviceChat && (
-
- {tx('send_message')}
-
+
+
+ {tx('send_message')}
+
+
)}
{statusText != '' && (
diff --git a/src/renderer/components/dialogs/ForwardMessage.tsx b/src/renderer/components/dialogs/_ForwardMessage.tsx
similarity index 84%
rename from src/renderer/components/dialogs/ForwardMessage.tsx
rename to src/renderer/components/dialogs/_ForwardMessage.tsx
index b8cf058469..384dee8ee5 100644
--- a/src/renderer/components/dialogs/ForwardMessage.tsx
+++ b/src/renderer/components/dialogs/_ForwardMessage.tsx
@@ -19,15 +19,21 @@ import useDialog from '../../hooks/useDialog'
import type { DialogProps } from '../../contexts/DialogContext'
-export default function ForwardMessage(props: {
- message: T.Message
+type ForwardMessageProps = {
+ messages: T.Message | number[]
onClose: DialogProps['onClose']
-}) {
+ onForward?: () => void
+}
+
+export default function ForwardMessage({
+ messages,
+ onClose,
+ onForward,
+}: ForwardMessageProps) {
const accountId = selectedAccountId()
const tx = useTranslationFunction()
const { openDialog } = useDialog()
- const { message, onClose } = props
const listFlags = C.DC_GCL_FOR_FORWARDING | C.DC_GCL_NO_SPECIALS
const { chatListIds, queryStr, setQueryStr } = useChatList(listFlags)
const { isChatLoaded, loadChats, chatCache } = useLogicVirtualChatList(
@@ -37,20 +43,36 @@ export default function ForwardMessage(props: {
const onChatClick = async (chatId: number) => {
const chat = await BackendRemote.rpc.getFullChatById(accountId, chatId)
+ const isMany = Array.isArray(messages)
onClose()
if (!chat.isSelfTalk) {
selectChat(chat.id)
const yes = await confirmForwardMessage(
openDialog,
accountId,
- message,
+ messages,
chat
)
if (!yes) {
- selectChat(message.chatId)
+ if (isMany) {
+ const message_ = await BackendRemote.rpc.getMessage(
+ accountId,
+ messages[0]
+ )
+ selectChat(message_.chatId)
+ } else {
+ selectChat(messages.chatId)
+ }
}
} else {
- await forwardMessage(accountId, message.id, chat.id)
+ if (isMany) {
+ for (const messageId of messages) {
+ await forwardMessage(accountId, messageId, chat.id)
+ }
+ } else {
+ await forwardMessage(accountId, messages.id, chat.id)
+ }
+ onForward && onForward()
}
}
const onSearchChange = (e: React.ChangeEvent) =>
diff --git a/src/renderer/components/message/Message.tsx b/src/renderer/components/message/Message.tsx
index a91ac308c6..3488be660e 100644
--- a/src/renderer/components/message/Message.tsx
+++ b/src/renderer/components/message/Message.tsx
@@ -1,4 +1,4 @@
-import React, { useContext } from 'react'
+import React, { useContext, useMemo } from 'react'
import reactStringReplace from 'react-string-replace'
import classNames from 'classnames'
import { C, T } from '@deltachat/jsonrpc-client'
@@ -41,6 +41,9 @@ import { ScreenContext } from '../../contexts/ScreenContext'
import useDialog from '../../hooks/useDialog'
import EnterAutocryptSetupMessage from '../dialogs/EnterAutocryptSetupMessage'
import { OpenDialog } from '../../contexts/DialogContext'
+import SelectModeOverlay from './SelectModeOverlay'
+import Button from '../ui/Button'
+import useSelectedMessages from '../../hooks/useSelectedMessages'
const Avatar = (
contact: Type.Contact,
@@ -137,12 +140,14 @@ function buildContextMenu(
conversationType,
openDialog,
chat,
+ selectMessage,
}: {
message: Type.Message | null
text?: string
conversationType: ConversationType
openDialog: OpenDialog
chat: T.FullChat
+ selectMessage: () => void
},
clickTarget: HTMLAnchorElement | null
): (false | ContextMenuItem)[] {
@@ -282,15 +287,22 @@ function buildContextMenu(
label: tx('delete_message_desktop'),
action: confirmDeleteMessage.bind(null, openDialog, message),
},
+ // Select
+ {
+ label: tx('select'),
+ action: selectMessage,
+ },
]
}
-export default function Message(props: {
+type MessageProps = {
message: Type.Message
conversationType: ConversationType
-}) {
- const { message, conversationType } = props
- const { id, viewType, text, hasLocation, isSetupmessage, hasHtml } = message
+}
+
+export default function Message({ message, conversationType }: MessageProps) {
+ const { id, viewType, text, hasLocation, isSetupmessage, hasHtml, chatId } =
+ message
const direction = getDirection(message)
const status = mapCoreMsgStatus2String(message.state)
const tx = useTranslationFunction()
@@ -299,11 +311,17 @@ export default function Message(props: {
const screenContext = useContext(ScreenContext)
const { openDialog } = useDialog()
const { openContextMenu } = screenContext
+ const { selectedMessages, selectMessage } = useSelectedMessages(chatId)
+ const isSelectMode = useMemo(
+ () => selectedMessages.length !== 0,
+ [selectedMessages]
+ )
const showMenu: (
event: React.MouseEvent
) => Promise = async event => {
event.preventDefault() // prevent default runtime context menu from opening
+ if (isSelectMode) return
const chat = await BackendRemote.rpc.getFullChatById(
accountId,
@@ -319,6 +337,7 @@ export default function Message(props: {
conversationType,
openDialog,
chat,
+ selectMessage: selectMessage.bind(null, message.id),
},
target
)
@@ -494,6 +513,9 @@ export default function Message(props: {
)}
id={message.id.toString()}
>
+ {isSelectMode && (
+
+ )}
{showAuthor &&
direction === 'incoming' &&
Avatar(message.sender, onContactClick)}
@@ -538,6 +560,7 @@ export default function Message(props: {
conversationType={conversationType}
message={message}
hasQuote={message.quote !== null}
+ isSelectMode={isSelectMode}
/>
)}
{message.viewType === 'Webxdc' && (
@@ -684,12 +707,9 @@ function WebxdcMessageContent({ message }: { message: Type.Message }) {
(only works in saved messages)
)}
- openWebxdc(message.id)}
- >
+ openWebxdc(message.id)}>
{tx('start_app')}
-
+
)
}
diff --git a/src/renderer/components/message/MessageList.tsx b/src/renderer/components/message/MessageList.tsx
index 9f99b570dd..c3b53fe961 100644
--- a/src/renderer/components/message/MessageList.tsx
+++ b/src/renderer/components/message/MessageList.tsx
@@ -92,13 +92,15 @@ function useUnreadCount(
return freshMessageCounter
}
+type MessageListProps = {
+ chatStore: ChatStoreStateWithChatSet
+ refComposer: todo
+}
+
export default function MessageList({
chatStore,
refComposer,
-}: {
- chatStore: ChatStoreStateWithChatSet
- refComposer: todo
-}) {
+}: MessageListProps) {
const accountId = selectedAccountId()
const {
store: {
diff --git a/src/renderer/components/message/MessageListAndComposer.tsx b/src/renderer/components/message/MessageListAndComposer.tsx
index 75f25cef82..4662fe1f70 100644
--- a/src/renderer/components/message/MessageListAndComposer.tsx
+++ b/src/renderer/components/message/MessageListAndComposer.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useEffect, useCallback } from 'react'
+import React, { useRef, useEffect, useCallback, useReducer } from 'react'
import { join, parse } from 'path'
import { Viewtype } from '@deltachat/jsonrpc-client/dist/generated/types'
@@ -15,6 +15,7 @@ import { sendMessage } from '../helpers/ChatMethods'
import useDialog from '../../hooks/useDialog'
import ConfirmSendingFiles from '../dialogs/ConfirmSendingFiles'
import useIsChatDisabled from '../composer/useIsChatDisabled'
+import SelectedMessagesAction from './SelectedMessagesAction'
const log = getLogger('renderer/MessageListAndComposer')
@@ -230,6 +231,7 @@ export default function MessageListAndComposer({
onDrop={onDrop.bind({ props: { chat: chatStore } })}
onDragOver={onDragOver}
>
+
diff --git a/src/renderer/components/message/MessageWrapper.tsx b/src/renderer/components/message/MessageWrapper.tsx
index ef2dd43574..86d66d98b6 100644
--- a/src/renderer/components/message/MessageWrapper.tsx
+++ b/src/renderer/components/message/MessageWrapper.tsx
@@ -16,6 +16,7 @@ type RenderMessageProps = {
export function MessageWrapper(props: RenderMessageProps) {
const state = props.message.state
+ const { key2, unreadMessageInViewIntersectionObserver } = props
const shouldInViewObserve =
state === C.DC_STATE_IN_FRESH || state === C.DC_STATE_IN_NOTICED
@@ -25,44 +26,40 @@ export function MessageWrapper(props: RenderMessageProps) {
if (!shouldInViewObserve) return
log.debug(
- `MessageWrapper: key: ${props.key2} We should observe this message if in view`
+ `MessageWrapper: key: ${key2} We should observe this message if in view`
)
- const messageBottomElement = document.querySelector('#bottom-' + props.key2)
+ const messageBottomElement = document.querySelector('#bottom-' + key2)
if (!messageBottomElement) {
log.error(
- `MessageWrapper: key: ${props.key2} couldn't find dom element. Returning`
+ `MessageWrapper: key: ${key2} couldn't find dom element. Returning`
)
return
}
if (
- !props.unreadMessageInViewIntersectionObserver.current ||
- !props.unreadMessageInViewIntersectionObserver.current.observe
+ !unreadMessageInViewIntersectionObserver.current ||
+ !unreadMessageInViewIntersectionObserver.current.observe
) {
log.error(
- `MessageWrapper: key: ${props.key2} unreadMessageInViewIntersectionObserver is null. Returning`
+ `MessageWrapper: key: ${key2} unreadMessageInViewIntersectionObserver is null. Returning`
)
return
}
- props.unreadMessageInViewIntersectionObserver.current.observe(
+ unreadMessageInViewIntersectionObserver.current.observe(
messageBottomElement
)
- log.debug(`MessageWrapper: key: ${props.key2} Successfully observing ;)`)
+ log.debug(`MessageWrapper: key: ${key2} Successfully observing ;)`)
return () =>
- props.unreadMessageInViewIntersectionObserver.current?.unobserve(
+ unreadMessageInViewIntersectionObserver.current?.unobserve(
messageBottomElement
)
- }, [
- props.key2,
- props.unreadMessageInViewIntersectionObserver,
- shouldInViewObserve,
- ])
+ }, [key2, unreadMessageInViewIntersectionObserver, shouldInViewObserve])
return (
-
+
-
+
)
}
diff --git a/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.scss b/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.scss
new file mode 100644
index 0000000000..26c05acb25
--- /dev/null
+++ b/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.scss
@@ -0,0 +1,12 @@
+.select-mode-overlay {
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ border-radius: 16px;
+ background-color: transparent;
+ cursor: grab;
+ &.selected {
+ background-color: var(--messageHightlightColor);
+ }
+}
diff --git a/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.tsx b/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.tsx
new file mode 100644
index 0000000000..4516d551c0
--- /dev/null
+++ b/src/renderer/components/message/SelectModeOverlay/SelectModeOverlay.tsx
@@ -0,0 +1,33 @@
+import React, { useMemo } from 'react'
+import classNames from 'classnames'
+import useSelectedMessages from '../../hooks/useSelectedMessages'
+import { MessageId, ChatId } from '../../contexts/SelectedMessagesContext'
+import './SelectModeOverlay.scss'
+
+type SelectModeOverlayProps = {
+ messageId: MessageId
+ chatId: ChatId
+}
+
+export default function SelectModeOverlay({
+ messageId,
+ chatId,
+}: SelectModeOverlayProps) {
+ const { selectedMessages, selectMessage, unselectMessage } =
+ useSelectedMessages(chatId)
+ const isSelected = useMemo(
+ () => selectedMessages.includes(messageId),
+ [selectedMessages]
+ )
+
+ return (
+
+ )
+}
diff --git a/src/renderer/components/message/SelectModeOverlay/index.ts b/src/renderer/components/message/SelectModeOverlay/index.ts
new file mode 100644
index 0000000000..ea191cf072
--- /dev/null
+++ b/src/renderer/components/message/SelectModeOverlay/index.ts
@@ -0,0 +1,3 @@
+import SelectModeOverlay from './SelectModeOverlay'
+
+export default SelectModeOverlay
diff --git a/src/renderer/components/message/SelectedMessagesAction.tsx b/src/renderer/components/message/SelectedMessagesAction.tsx
new file mode 100644
index 0000000000..f209bc4693
--- /dev/null
+++ b/src/renderer/components/message/SelectedMessagesAction.tsx
@@ -0,0 +1,43 @@
+import React from 'react'
+
+import useTranslationFunction from '../../hooks/useTranslationFunction'
+import { openForwardDialog, confirmDeleteMessage } from './messageFunctions'
+import useDialog from '../../hooks/useDialog'
+import useSelectedMessages from '../../hooks/useSelectedMessages'
+import { ChatId } from '../../contexts/SelectedMessagesContext'
+
+export default function SelectedMessagesAction({ chatId }: { chatId: ChatId }) {
+ const tx = useTranslationFunction()
+ const { openDialog } = useDialog()
+ const { selectedMessages, resetSelected } = useSelectedMessages(chatId)
+ if (selectedMessages.length === 0) return null
+
+ return (
+
+
+ openForwardDialog(openDialog, selectedMessages, resetSelected)
+ }
+ >
+ {tx('forward')}
+
+
+ confirmDeleteMessage(openDialog, selectedMessages, resetSelected)
+ }
+ >
+ {tx('delete')}
+
+
+ {tx('cancel')}
+
+
+ {tx('n_selected', [selectedMessages.length.toLocaleString()], {
+ quantity: selectedMessages.length,
+ })}
+
+
+ )
+}
diff --git a/src/renderer/components/message/messageFunctions.ts b/src/renderer/components/message/messageFunctions.ts
index 095ffb62c7..b38cf4745c 100644
--- a/src/renderer/components/message/messageFunctions.ts
+++ b/src/renderer/components/message/messageFunctions.ts
@@ -47,9 +47,10 @@ export function openAttachmentInShell(msg: Type.Message) {
export function openForwardDialog(
openDialog: OpenDialog,
- message: Type.Message
+ message: Type.Message | number[],
+ onForward?: () => void
) {
- openDialog(ForwardMessage, { message })
+ openDialog(ForwardMessage, { message, onForward })
}
export function confirmDialog(
@@ -73,7 +74,7 @@ export function confirmDialog(
export async function confirmForwardMessage(
openDialog: OpenDialog,
accountId: number,
- message: Type.Message,
+ messages: Type.Message | number[],
chat: Type.FullChat
) {
const tx = window.static_translate
@@ -82,22 +83,41 @@ export async function confirmForwardMessage(
tx('ask_forward', [chat.name]),
tx('forward')
)
+ const isMany = Array.isArray(messages)
if (yes) {
- await forwardMessage(accountId, message.id, chat.id)
+ if (isMany) {
+ for (const messageId of messages)
+ await forwardMessage(accountId, messageId, chat.id)
+ } else {
+ await forwardMessage(accountId, messages.id, chat.id)
+ }
}
return yes
}
export function confirmDeleteMessage(
openDialog: OpenDialog,
- msg: Type.Message
+ messages: Type.Message | number[],
+ onDelete?: () => void
) {
const tx = window.static_translate
openDialog(ConfirmationDialog, {
message: tx('ask_delete_message'),
confirmLabel: tx('delete'),
- cb: (yes: boolean) => yes && deleteMessage(msg.id),
+ cb: (yes: boolean) => {
+ if (yes) {
+ const isMany = Array.isArray(messages)
+ if (isMany) {
+ for (const messageId of messages) {
+ deleteMessage(messageId)
+ }
+ } else {
+ deleteMessage(messages.id)
+ }
+ onDelete && onDelete()
+ }
+ },
})
}
diff --git a/src/renderer/components/screens/WelcomeScreen.tsx b/src/renderer/components/screens/WelcomeScreen.tsx
index c660880e7c..52cbd2dcb7 100644
--- a/src/renderer/components/screens/WelcomeScreen.tsx
+++ b/src/renderer/components/screens/WelcomeScreen.tsx
@@ -19,6 +19,7 @@ import useTranslationFunction from '../../hooks/useTranslationFunction'
import useDialog from '../../hooks/useDialog'
import ImportQrCode from '../dialogs/ImportQrCode'
import AlertDialog from '../dialogs/AlertDialog'
+import Button from '../ui/Button'
const log = getLogger('renderer/components/AccountsScreen')
@@ -105,12 +106,9 @@ const ImportButton = function ImportButton() {
}
return (
-
+
{tx('import_backup_title')}
-
+
)
}
@@ -190,25 +188,19 @@ export default function WelcomeScreen({
{tx('welcome_chat_over_email')}
-
window.__changeScreen(Screens.Login)}
>
{tx('login_header')}
-
-
+
+
{tx('multidevice_receiver_title')}
-
-
+
+
{tx('scan_invitation_code')}
-
+
diff --git a/src/renderer/components/ui/Button.tsx b/src/renderer/components/ui/Button.tsx
new file mode 100644
index 0000000000..393886cafc
--- /dev/null
+++ b/src/renderer/components/ui/Button.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import classNames from 'classnames'
+
+type ButtonProps = React.PropsWithChildren<{
+ type?: 'secondary' | 'primary' | 'danger'
+ onClick: any
+ round?: boolean
+ id?: string
+ 'aria-label'?: string
+ disabled?: boolean
+ className?: any
+}>
+
+export default function Button({
+ children,
+ className,
+ disabled,
+ type,
+ round,
+ onClick,
+ id,
+ ...props
+}: ButtonProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/renderer/components/Radio.tsx b/src/renderer/components/ui/Radio.tsx
similarity index 100%
rename from src/renderer/components/Radio.tsx
rename to src/renderer/components/ui/Radio.tsx
diff --git a/src/renderer/components/RadioGroup.tsx b/src/renderer/components/ui/RadioGroup.tsx
similarity index 100%
rename from src/renderer/components/RadioGroup.tsx
rename to src/renderer/components/ui/RadioGroup.tsx
diff --git a/src/renderer/contexts/SelectedMessagesContext.tsx b/src/renderer/contexts/SelectedMessagesContext.tsx
new file mode 100644
index 0000000000..9423c14c48
--- /dev/null
+++ b/src/renderer/contexts/SelectedMessagesContext.tsx
@@ -0,0 +1,15 @@
+import { createContext } from 'react'
+
+export type MessageId = number
+export type ChatId = number
+
+export type SelectedMessagesValue = {
+ selectedMessages: Record
+ selectMessage: (chatId: ChatId, msgId: MessageId) => void
+ unselectMessage: (chatId: ChatId, msgId: MessageId) => void
+ resetSelected: (chatId: ChatId) => void
+}
+
+const SelectedMessagesContext = createContext(null)
+
+export default SelectedMessagesContext
diff --git a/src/renderer/hooks/useSelectedMessages.tsx b/src/renderer/hooks/useSelectedMessages.tsx
new file mode 100644
index 0000000000..a55b193329
--- /dev/null
+++ b/src/renderer/hooks/useSelectedMessages.tsx
@@ -0,0 +1,74 @@
+import { useReducer, useCallback } from 'react'
+import {
+ MessageId,
+ ChatId,
+} from '../contexts/SelectedMessagesContext'
+
+export type SelectedMessagesPerChatValue = {
+ selectedMessages: MessageId[]
+ selectMessage: (msgId: MessageId) => void
+ unselectMessage: (msgId: MessageId) => void
+ resetSelected: () => void
+}
+
+
+export default function useSelectedMessages(
+ chatId: MessageId
+): SelectedMessagesPerChatValue {
+ type MessageAction =
+ | {
+ type_: 'select' | 'unselect'
+ messageId: MessageId
+ }
+ | { type_: 'reset'; messageId?: never }
+ const [selectedMessagesValue, _dispatch] = useReducer(
+ (
+ selectedMessagesPerChat: Record,
+ action: MessageAction
+ ) => {
+ const selectedMessages = selectedMessagesPerChat[chatId] || []
+ switch (action.type_) {
+ case 'select':
+ selectedMessagesPerChat[chatId] = [
+ ...selectedMessages,
+ action.messageId,
+ ]
+ break
+ case 'unselect':
+ selectedMessagesPerChat[chatId] = selectedMessages.filter(
+ id => id !== action.messageId
+ )
+ break
+ case 'reset':
+ selectedMessagesPerChat[chatId] = []
+ break
+ }
+ return selectedMessagesPerChat
+ },
+ {}
+ )
+
+ const selectMessage = useCallback<(id: MessageId) => void>(
+ (id: MessageId) => _dispatch({ messageId: id, type_: 'select' }),
+ [_dispatch, chatId]
+ )
+
+ const unselectMessage = useCallback<(id: MessageId) => void>(
+ (id: MessageId) => _dispatch({ messageId: id, type_: 'unselect' }),
+ [_dispatch, chatId]
+ )
+
+ const resetSelected = useCallback<() => void>(
+ () => _dispatch({ type_: 'reset' }),
+ [_dispatch, chatId]
+ )
+
+ const value: SelectedMessagesPerChatValue = {
+ selectedMessages: selectedMessagesValue[chatId] || [],
+ selectMessage,
+ unselectMessage,
+ resetSelected,
+ }
+
+ return value
+}