Adds message forwarding
This commit is contained in:
parent
cd489a35fd
commit
d203f125c6
42 changed files with 1638 additions and 139 deletions
|
@ -7,6 +7,7 @@ import { actions as conversations } from './ducks/conversations';
|
|||
import { actions as emojis } from './ducks/emojis';
|
||||
import { actions as expiration } from './ducks/expiration';
|
||||
import { actions as items } from './ducks/items';
|
||||
import { actions as linkPreviews } from './ducks/linkPreviews';
|
||||
import { actions as network } from './ducks/network';
|
||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
import { actions as search } from './ducks/search';
|
||||
|
@ -21,6 +22,7 @@ export const mapDispatchToProps = {
|
|||
...emojis,
|
||||
...expiration,
|
||||
...items,
|
||||
...linkPreviews,
|
||||
...network,
|
||||
...safetyNumber,
|
||||
...search,
|
||||
|
|
|
@ -108,6 +108,7 @@ export type ConversationType = {
|
|||
// This is used by the CompositionInput for @mentions
|
||||
sortedGroupMembers?: Array<ConversationType>;
|
||||
title: string;
|
||||
searchableTitle?: string;
|
||||
unreadCount?: number;
|
||||
isSelected?: boolean;
|
||||
typingContact?: {
|
||||
|
|
|
@ -2,11 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StateType } from '../reducer';
|
||||
import * as storageShim from '../../shims/storage';
|
||||
import { isShortName } from '../../components/emoji/lib';
|
||||
import { useBoundActions } from '../../util/hooks';
|
||||
|
||||
// State
|
||||
|
@ -54,6 +50,7 @@ export type ItemsActionType =
|
|||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
onSetSkinTone,
|
||||
putItem,
|
||||
putItemExternal,
|
||||
removeItem,
|
||||
|
@ -72,6 +69,10 @@ function putItem(key: string, value: unknown): ItemPutAction {
|
|||
};
|
||||
}
|
||||
|
||||
function onSetSkinTone(tone: number): ItemPutAction {
|
||||
return putItem('skinTone', tone);
|
||||
}
|
||||
|
||||
function putItemExternal(key: string, value: unknown): ItemPutExternalAction {
|
||||
return {
|
||||
type: 'items/PUT_EXTERNAL',
|
||||
|
@ -133,13 +134,3 @@ export function reducer(
|
|||
|
||||
return state;
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
||||
const selectRecentEmojis = createSelector(
|
||||
({ emojis }: StateType) => emojis.recents,
|
||||
recents => recents.filter(isShortName)
|
||||
);
|
||||
|
||||
export const useRecentEmojis = (): Array<string> =>
|
||||
useSelector(selectRecentEmojis);
|
||||
|
|
77
ts/state/ducks/linkPreviews.ts
Normal file
77
ts/state/ducks/linkPreviews.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
|
||||
// State
|
||||
|
||||
export type LinkPreviewsStateType = {
|
||||
readonly linkPreview?: LinkPreviewType;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
||||
const ADD_PREVIEW = 'linkPreviews/ADD_PREVIEW';
|
||||
const REMOVE_PREVIEW = 'linkPreviews/REMOVE_PREVIEW';
|
||||
|
||||
type AddLinkPreviewActionType = {
|
||||
type: 'linkPreviews/ADD_PREVIEW';
|
||||
payload: LinkPreviewType;
|
||||
};
|
||||
|
||||
type RemoveLinkPreviewActionType = {
|
||||
type: 'linkPreviews/REMOVE_PREVIEW';
|
||||
};
|
||||
|
||||
type LinkPreviewsActionType =
|
||||
| AddLinkPreviewActionType
|
||||
| RemoveLinkPreviewActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
addLinkPreview,
|
||||
removeLinkPreview,
|
||||
};
|
||||
|
||||
function addLinkPreview(payload: LinkPreviewType): AddLinkPreviewActionType {
|
||||
return {
|
||||
type: ADD_PREVIEW,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function removeLinkPreview(): RemoveLinkPreviewActionType {
|
||||
return {
|
||||
type: REMOVE_PREVIEW,
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): LinkPreviewsStateType {
|
||||
return {
|
||||
linkPreview: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<LinkPreviewsStateType> = getEmptyState(),
|
||||
action: Readonly<LinkPreviewsActionType>
|
||||
): LinkPreviewsStateType {
|
||||
if (action.type === ADD_PREVIEW) {
|
||||
const { payload } = action;
|
||||
|
||||
return {
|
||||
linkPreview: payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === REMOVE_PREVIEW) {
|
||||
return {
|
||||
linkPreview: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -9,6 +9,7 @@ import { reducer as conversations } from './ducks/conversations';
|
|||
import { reducer as emojis } from './ducks/emojis';
|
||||
import { reducer as expiration } from './ducks/expiration';
|
||||
import { reducer as items } from './ducks/items';
|
||||
import { reducer as linkPreviews } from './ducks/linkPreviews';
|
||||
import { reducer as network } from './ducks/network';
|
||||
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
||||
import { reducer as search } from './ducks/search';
|
||||
|
@ -23,6 +24,7 @@ export const reducer = combineReducers({
|
|||
emojis,
|
||||
expiration,
|
||||
items,
|
||||
linkPreviews,
|
||||
network,
|
||||
safetyNumber,
|
||||
search,
|
||||
|
|
21
ts/state/roots/createForwardMessageModal.tsx
Normal file
21
ts/state/roots/createForwardMessageModal.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import {
|
||||
SmartForwardMessageModal,
|
||||
SmartForwardMessageModalProps,
|
||||
} from '../smart/ForwardMessageModal';
|
||||
|
||||
export const createForwardMessageModal = (
|
||||
store: Store,
|
||||
props: SmartForwardMessageModalProps
|
||||
): React.ReactElement => (
|
||||
<Provider store={store}>
|
||||
<SmartForwardMessageModal {...props} />
|
||||
</Provider>
|
||||
);
|
|
@ -18,7 +18,6 @@ import {
|
|||
OneTimeModalState,
|
||||
PreJoinConversationType,
|
||||
} from '../ducks/conversations';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { deconstructLookup } from '../../util/deconstructLookup';
|
||||
import type { CallsByConversationType } from '../ducks/calling';
|
||||
|
@ -350,6 +349,29 @@ function canComposeConversation(conversation: ConversationType): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export const getAllComposableConversations = createSelector(
|
||||
getConversationLookup,
|
||||
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
||||
Object.values(conversationLookup).filter(
|
||||
contact =>
|
||||
!contact.isBlocked &&
|
||||
!isConversationUnregistered(contact) &&
|
||||
(isString(contact.name) || contact.profileSharing)
|
||||
)
|
||||
);
|
||||
|
||||
const getContactsAndMe = createSelector(
|
||||
getConversationLookup,
|
||||
(conversationLookup: ConversationLookupType): Array<ConversationType> =>
|
||||
Object.values(conversationLookup).filter(
|
||||
contact =>
|
||||
contact.type === 'direct' &&
|
||||
!contact.isBlocked &&
|
||||
!isConversationUnregistered(contact) &&
|
||||
(isString(contact.name) || contact.profileSharing)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* This returns contacts for the composer and group members, which isn't just your primary
|
||||
* system contacts. It may include false positives, which is better than missing contacts.
|
||||
|
@ -381,29 +403,14 @@ const getNormalizedComposerConversationSearchTerm = createSelector(
|
|||
(searchTerm: string): string => searchTerm.trim()
|
||||
);
|
||||
|
||||
const getNoteToSelfTitle = createSelector(getIntl, (i18n: LocalizerType) =>
|
||||
i18n('noteToSelf').toLowerCase()
|
||||
);
|
||||
|
||||
export const getComposeContacts = createSelector(
|
||||
getNormalizedComposerConversationSearchTerm,
|
||||
getComposableContacts,
|
||||
getMe,
|
||||
getNoteToSelfTitle,
|
||||
getContactsAndMe,
|
||||
(
|
||||
searchTerm: string,
|
||||
contacts: Array<ConversationType>,
|
||||
noteToSelf: ConversationType,
|
||||
noteToSelfTitle: string
|
||||
contacts: Array<ConversationType>
|
||||
): Array<ConversationType> => {
|
||||
const result: Array<ConversationType> = filterAndSortConversations(
|
||||
contacts,
|
||||
searchTerm
|
||||
);
|
||||
if (!searchTerm || noteToSelfTitle.includes(searchTerm)) {
|
||||
result.push(noteToSelf);
|
||||
}
|
||||
return result;
|
||||
return filterAndSortConversations(contacts, searchTerm);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
16
ts/state/selectors/emojis.ts
Normal file
16
ts/state/selectors/emojis.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
import { isShortName } from '../../components/emoji/lib';
|
||||
|
||||
export const selectRecentEmojis = createSelector(
|
||||
({ emojis }: StateType) => emojis.recents,
|
||||
recents => recents.filter(isShortName)
|
||||
);
|
||||
|
||||
export const useRecentEmojis = (): Array<string> =>
|
||||
useSelector(selectRecentEmojis);
|
21
ts/state/selectors/linkPreviews.ts
Normal file
21
ts/state/selectors/linkPreviews.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
export const getLinkPreview = createSelector(
|
||||
({ linkPreviews }: StateType) => linkPreviews.linkPreview,
|
||||
linkPreview => {
|
||||
if (linkPreview) {
|
||||
return {
|
||||
...linkPreview,
|
||||
domain: window.Signal.LinkPreviews.getDomain(linkPreview.url),
|
||||
isLoaded: true,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
);
|
|
@ -2,13 +2,12 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { get } from 'lodash';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { CompositionArea } from '../../components/CompositionArea';
|
||||
import { StateType } from '../reducer';
|
||||
|
||||
import { isShortName } from '../../components/emoji/lib';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
|
@ -24,11 +23,6 @@ type ExternalProps = {
|
|||
id: string;
|
||||
};
|
||||
|
||||
const selectRecentEmojis = createSelector(
|
||||
({ emojis }: StateType) => emojis.recents,
|
||||
recents => recents.filter(isShortName)
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { get } from 'lodash';
|
||||
import { StateType } from '../reducer';
|
||||
import { useActions as useItemActions, useRecentEmojis } from '../ducks/items';
|
||||
import { useRecentEmojis } from '../selectors/emojis';
|
||||
import { useActions as useEmojiActions } from '../ducks/emojis';
|
||||
|
||||
import {
|
||||
|
@ -17,8 +17,8 @@ import { LocalizerType } from '../../types/Util';
|
|||
|
||||
export const SmartEmojiPicker = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
Pick<EmojiPickerProps, 'onPickEmoji' | 'onClose' | 'style'>
|
||||
>(({ onPickEmoji, onClose, style }, ref) => {
|
||||
Pick<EmojiPickerProps, 'onPickEmoji' | 'onSetSkinTone' | 'onClose' | 'style'>
|
||||
>(({ onPickEmoji, onSetSkinTone, onClose, style }, ref) => {
|
||||
const i18n = useSelector<StateType, LocalizerType>(getIntl);
|
||||
const skinTone = useSelector<StateType, number>(state =>
|
||||
get(state, ['items', 'skinTone'], 0)
|
||||
|
@ -26,15 +26,6 @@ export const SmartEmojiPicker = React.forwardRef<
|
|||
|
||||
const recentEmojis = useRecentEmojis();
|
||||
|
||||
const { putItem } = useItemActions();
|
||||
|
||||
const onSetSkinTone = React.useCallback(
|
||||
tone => {
|
||||
putItem('skinTone', tone);
|
||||
},
|
||||
[putItem]
|
||||
);
|
||||
|
||||
const { onUseEmoji } = useEmojiActions();
|
||||
|
||||
const handlePickEmoji = React.useCallback(
|
||||
|
|
79
ts/state/smart/ForwardMessageModal.tsx
Normal file
79
ts/state/smart/ForwardMessageModal.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { get } from 'lodash';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import {
|
||||
ForwardMessageModal,
|
||||
DataPropsType,
|
||||
} from '../../components/ForwardMessageModal';
|
||||
import { StateType } from '../reducer';
|
||||
import { BodyRangeType } from '../../types/Util';
|
||||
import { LinkPreviewType } from '../../types/message/LinkPreviews';
|
||||
import { getAllComposableConversations } from '../selectors/conversations';
|
||||
import { getLinkPreview } from '../selectors/linkPreviews';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
|
||||
export type SmartForwardMessageModalProps = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
doForwardMessage: (
|
||||
selectedContacts: Array<string>,
|
||||
messageBody?: string,
|
||||
attachments?: Array<AttachmentType>,
|
||||
linkPreview?: LinkPreviewType
|
||||
) => void;
|
||||
isSticker: boolean;
|
||||
messageBody?: string;
|
||||
onClose: () => void;
|
||||
onEditorStateChange: (
|
||||
messageText: string,
|
||||
bodyRanges: Array<BodyRangeType>,
|
||||
caretLocation?: number
|
||||
) => unknown;
|
||||
onTextTooLong: () => void;
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: SmartForwardMessageModalProps
|
||||
): DataPropsType => {
|
||||
const {
|
||||
attachments,
|
||||
doForwardMessage,
|
||||
isSticker,
|
||||
messageBody,
|
||||
onClose,
|
||||
onEditorStateChange,
|
||||
onTextTooLong,
|
||||
} = props;
|
||||
|
||||
const candidateConversations = getAllComposableConversations(state);
|
||||
const recentEmojis = selectRecentEmojis(state);
|
||||
const skinTone = get(state, ['items', 'skinTone'], 0);
|
||||
const linkPreview = getLinkPreview(state);
|
||||
|
||||
return {
|
||||
attachments,
|
||||
candidateConversations,
|
||||
doForwardMessage,
|
||||
i18n: getIntl(state),
|
||||
isSticker,
|
||||
linkPreview,
|
||||
messageBody,
|
||||
onClose,
|
||||
onEditorStateChange,
|
||||
recentEmojis,
|
||||
skinTone,
|
||||
onTextTooLong,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, {
|
||||
...mapDispatchToProps,
|
||||
onPickEmoji: mapDispatchToProps.onUseEmoji,
|
||||
});
|
||||
|
||||
export const SmartForwardMessageModal = smart(ForwardMessageModal);
|
|
@ -42,6 +42,7 @@ export type OwnProps = {
|
|||
| 'showContactModal'
|
||||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showForwardMessageModal'
|
||||
| 'showVisualAttachment'
|
||||
>;
|
||||
|
||||
|
@ -72,6 +73,7 @@ const mapStateToProps = (
|
|||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showForwardMessageModal,
|
||||
showVisualAttachment,
|
||||
} = props;
|
||||
|
||||
|
@ -103,6 +105,7 @@ const mapStateToProps = (
|
|||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showForwardMessageModal,
|
||||
showVisualAttachment,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import { actions as conversations } from './ducks/conversations';
|
|||
import { actions as emojis } from './ducks/emojis';
|
||||
import { actions as expiration } from './ducks/expiration';
|
||||
import { actions as items } from './ducks/items';
|
||||
import { actions as linkPreviews } from './ducks/linkPreviews';
|
||||
import { actions as network } from './ducks/network';
|
||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
import { actions as search } from './ducks/search';
|
||||
|
@ -21,6 +22,7 @@ export type ReduxActions = {
|
|||
emojis: typeof emojis;
|
||||
expiration: typeof expiration;
|
||||
items: typeof items;
|
||||
linkPreviews: typeof linkPreviews;
|
||||
network: typeof network;
|
||||
safetyNumber: typeof safetyNumber;
|
||||
search: typeof search;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue