Adds message forwarding

This commit is contained in:
Josh Perez 2021-04-27 15:35:35 -07:00 committed by GitHub
parent cd489a35fd
commit d203f125c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1638 additions and 139 deletions

View file

@ -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,

View file

@ -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?: {

View file

@ -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);

View 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;
}

View file

@ -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,

View 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>
);

View file

@ -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);
}
);

View 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);

View 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;
}
);

View file

@ -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;

View file

@ -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(

View 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);

View file

@ -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,
};
};

View file

@ -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;