Scroll to bottom of conversation on message send
This commit is contained in:
parent
254c87a1ac
commit
5bd7eda124
13 changed files with 107 additions and 21 deletions
|
@ -63,6 +63,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
draftText: overrideProps.draftText || undefined,
|
||||
clearQuotedMessage: action('clearQuotedMessage'),
|
||||
getQuotedMessage: action('getQuotedMessage'),
|
||||
scrollToBottom: action('scrollToBottom'),
|
||||
sortedGroupMembers: [],
|
||||
// EmojiButton
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
|
|
|
@ -118,6 +118,7 @@ export type OwnProps = Readonly<{
|
|||
setQuotedMessage(message: undefined): unknown;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
startRecording: () => unknown;
|
||||
scrollToBottom: (converstionId: string) => unknown;
|
||||
theme: ThemeType;
|
||||
}>;
|
||||
|
||||
|
@ -196,6 +197,7 @@ export const CompositionArea = ({
|
|||
draftBodyRanges,
|
||||
clearQuotedMessage,
|
||||
getQuotedMessage,
|
||||
scrollToBottom,
|
||||
sortedGroupMembers,
|
||||
// EmojiButton
|
||||
onPickEmoji,
|
||||
|
@ -622,19 +624,21 @@ export const CompositionArea = ({
|
|||
<div className="CompositionArea__input">
|
||||
<CompositionInput
|
||||
i18n={i18n}
|
||||
conversationId={conversationId}
|
||||
clearQuotedMessage={clearQuotedMessage}
|
||||
disabled={disabled}
|
||||
large={large}
|
||||
draftBodyRanges={draftBodyRanges}
|
||||
draftText={draftText}
|
||||
getQuotedMessage={getQuotedMessage}
|
||||
inputApi={inputApiRef}
|
||||
large={large}
|
||||
onDirtyChange={setDirty}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={handleSubmit}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onDirtyChange={setDirty}
|
||||
scrollToBottom={scrollToBottom}
|
||||
skinTone={skinTone}
|
||||
draftText={draftText}
|
||||
draftBodyRanges={draftBodyRanges}
|
||||
clearQuotedMessage={clearQuotedMessage}
|
||||
getQuotedMessage={getQuotedMessage}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -20,6 +20,7 @@ const story = storiesOf('Components/CompositionInput', module);
|
|||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
i18n,
|
||||
conversationId: 'conversation-id',
|
||||
disabled: boolean('disabled', overrideProps.disabled || false),
|
||||
onSubmit: action('onSubmit'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
|
@ -30,6 +31,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
getQuotedMessage: action('getQuotedMessage'),
|
||||
onPickEmoji: action('onPickEmoji'),
|
||||
large: boolean('large', overrideProps.large || false),
|
||||
scrollToBottom: action('scrollToBottom'),
|
||||
sortedGroupMembers: overrideProps.sortedGroupMembers || [],
|
||||
skinTone: select(
|
||||
'skinTone',
|
||||
|
|
|
@ -61,6 +61,7 @@ export type InputApi = {
|
|||
|
||||
export type Props = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly conversationId: string;
|
||||
readonly disabled?: boolean;
|
||||
readonly large?: boolean;
|
||||
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
|
||||
|
@ -84,6 +85,7 @@ export type Props = {
|
|||
): unknown;
|
||||
getQuotedMessage(): unknown;
|
||||
clearQuotedMessage(): unknown;
|
||||
scrollToBottom: (converstionId: string) => unknown;
|
||||
};
|
||||
|
||||
const MAX_LENGTH = 64 * 1024;
|
||||
|
@ -92,6 +94,7 @@ const BASE_CLASS_NAME = 'module-composition-input';
|
|||
export function CompositionInput(props: Props): React.ReactElement {
|
||||
const {
|
||||
i18n,
|
||||
conversationId,
|
||||
disabled,
|
||||
large,
|
||||
inputApi,
|
||||
|
@ -103,6 +106,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
draftBodyRanges,
|
||||
getQuotedMessage,
|
||||
clearQuotedMessage,
|
||||
scrollToBottom,
|
||||
sortedGroupMembers,
|
||||
} = props;
|
||||
|
||||
|
@ -238,6 +242,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
|||
`CompositionInput: Submitting message ${timestamp} with ${mentions.length} mentions`
|
||||
);
|
||||
onSubmit(text, mentions, timestamp);
|
||||
scrollToBottom(conversationId);
|
||||
};
|
||||
|
||||
if (inputApi) {
|
||||
|
|
|
@ -42,6 +42,7 @@ const candidateConversations = Array.from(Array(100), () =>
|
|||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
attachments: overrideProps.attachments,
|
||||
conversationId: 'conversation-id',
|
||||
candidateConversations,
|
||||
doForwardMessage: action('doForwardMessage'),
|
||||
i18n,
|
||||
|
|
|
@ -40,6 +40,7 @@ import { useAnimated } from '../hooks/useAnimated';
|
|||
export type DataPropsType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
candidateConversations: ReadonlyArray<ConversationType>;
|
||||
conversationId: string;
|
||||
doForwardMessage: (
|
||||
selectedContacts: Array<string>,
|
||||
messageBody?: string,
|
||||
|
@ -74,6 +75,7 @@ const MAX_FORWARD = 5;
|
|||
export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||
attachments,
|
||||
candidateConversations,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
i18n,
|
||||
isSticker,
|
||||
|
@ -181,10 +183,10 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
}, [candidateConversations]);
|
||||
|
||||
const toggleSelectedConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
(selectedConversationId: string) => {
|
||||
let removeContact = false;
|
||||
const nextSelectedContacts = selectedContacts.filter(contact => {
|
||||
if (contact.id === conversationId) {
|
||||
if (contact.id === selectedConversationId) {
|
||||
removeContact = true;
|
||||
return false;
|
||||
}
|
||||
|
@ -194,7 +196,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
setSelectedContacts(nextSelectedContacts);
|
||||
return;
|
||||
}
|
||||
const selectedContact = contactLookup.get(conversationId);
|
||||
const selectedContact = contactLookup.get(selectedConversationId);
|
||||
if (selectedContact) {
|
||||
if (selectedContact.announcementsOnly && !selectedContact.areWeAdmin) {
|
||||
setCannotMessage(true);
|
||||
|
@ -330,6 +332,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
) : null}
|
||||
<div className="module-ForwardMessageModal__text-edit-area">
|
||||
<CompositionInput
|
||||
conversationId={conversationId}
|
||||
clearQuotedMessage={shouldNeverBeCalled}
|
||||
draftText={messageBodyText}
|
||||
getQuotedMessage={noop}
|
||||
|
@ -337,6 +340,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
inputApi={inputApiRef}
|
||||
large
|
||||
moduleClassName="module-ForwardMessageModal__input"
|
||||
scrollToBottom={noop}
|
||||
onEditorStateChange={(
|
||||
messageText,
|
||||
bodyRanges,
|
||||
|
@ -391,7 +395,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(
|
||||
conversationId: string,
|
||||
selectedConversationId: string,
|
||||
disabledReason:
|
||||
| undefined
|
||||
| ContactCheckboxDisabledReason
|
||||
|
@ -400,7 +404,9 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
disabledReason !==
|
||||
ContactCheckboxDisabledReason.MaximumContactsSelected
|
||||
) {
|
||||
toggleSelectedConversation(conversationId);
|
||||
toggleSelectedConversation(
|
||||
selectedConversationId
|
||||
);
|
||||
}
|
||||
}}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
|
|
|
@ -466,7 +466,10 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
overrideProps.isLoadingMessages === false
|
||||
),
|
||||
items: overrideProps.items || Object.keys(items),
|
||||
loadCountdownStart: undefined,
|
||||
messageHeightChangeIndex: undefined,
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToIndex: overrideProps.scrollToIndex,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: number('totalUnread', overrideProps.totalUnread || 0),
|
||||
|
@ -478,6 +481,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
warning: overrideProps.warning,
|
||||
|
||||
id: uuid(),
|
||||
isNearBottom: false,
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
renderHeroRow,
|
||||
|
|
|
@ -77,13 +77,14 @@ export type PropsDataType = {
|
|||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
isLoadingMessages: boolean;
|
||||
isNearBottom?: boolean;
|
||||
isNearBottom: boolean;
|
||||
items: ReadonlyArray<string>;
|
||||
loadCountdownStart?: number;
|
||||
messageHeightChangeIndex?: number;
|
||||
oldestUnreadIndex?: number;
|
||||
loadCountdownStart: number | undefined;
|
||||
messageHeightChangeIndex: number | undefined;
|
||||
oldestUnreadIndex: number | undefined;
|
||||
resetCounter: number;
|
||||
scrollToIndex?: number;
|
||||
scrollToBottomCounter: number;
|
||||
scrollToIndex: number | undefined;
|
||||
scrollToIndexCounter: number;
|
||||
totalUnread: number;
|
||||
};
|
||||
|
@ -959,7 +960,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
this.scrollDown(false);
|
||||
};
|
||||
|
||||
public scrollDown = (setFocus?: boolean): void => {
|
||||
public scrollDown = (setFocus?: boolean, forceScrollDown?: boolean): void => {
|
||||
const {
|
||||
haveNewest,
|
||||
id,
|
||||
|
@ -976,7 +977,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
const lastId = items[items.length - 1];
|
||||
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
|
||||
|
||||
if (!this.visibleRows) {
|
||||
if (!this.visibleRows || forceScrollDown) {
|
||||
if (haveNewest) {
|
||||
this.scrollToBottom(setFocus);
|
||||
} else if (!isLoadingMessages) {
|
||||
|
@ -1033,6 +1034,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
messageHeightChangeIndex,
|
||||
oldestUnreadIndex,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToIndex,
|
||||
typingContact,
|
||||
} = this.props;
|
||||
|
@ -1050,6 +1052,10 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
this.resizeHeroRow();
|
||||
}
|
||||
|
||||
if (scrollToBottomCounter !== prevProps.scrollToBottomCounter) {
|
||||
this.scrollDown(false, true);
|
||||
}
|
||||
|
||||
// There are a number of situations which can necessitate that we forget about row
|
||||
// heights previously calculated. We reset the minimum number of rows to minimize
|
||||
// unexpected changes to the scroll position. Those changes happen because
|
||||
|
|
|
@ -245,6 +245,7 @@ export type ConversationMessageType = {
|
|||
messageIds: Array<string>;
|
||||
metrics: MessageMetricsType;
|
||||
resetCounter: number;
|
||||
scrollToBottomCounter: number;
|
||||
scrollToMessageId?: string;
|
||||
scrollToMessageCounter: number;
|
||||
};
|
||||
|
@ -592,6 +593,12 @@ export type SetSelectedConversationPanelDepthActionType = {
|
|||
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
|
||||
payload: { panelDepth: number };
|
||||
};
|
||||
export type ScrollToBpttomActionType = {
|
||||
type: 'SCROLL_TO_BOTTOM';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
};
|
||||
};
|
||||
export type ScrollToMessageActionType = {
|
||||
type: 'SCROLL_TO_MESSAGE';
|
||||
payload: {
|
||||
|
@ -741,6 +748,7 @@ export type ConversationActionType =
|
|||
| ReplaceAvatarsActionType
|
||||
| ReviewGroupMemberNameCollisionActionType
|
||||
| ReviewMessageRequestNameCollisionActionType
|
||||
| ScrollToBpttomActionType
|
||||
| ScrollToMessageActionType
|
||||
| SelectedConversationChangedActionType
|
||||
| SetComposeGroupAvatarActionType
|
||||
|
@ -810,6 +818,7 @@ export const actions = {
|
|||
reviewMessageRequestNameCollision,
|
||||
saveAvatarToDisk,
|
||||
saveUsername,
|
||||
scrollToBottom,
|
||||
scrollToMessage,
|
||||
selectMessage,
|
||||
setComposeGroupAvatar,
|
||||
|
@ -1685,6 +1694,15 @@ function closeMaximumGroupSizeModal(): CloseMaximumGroupSizeModalActionType {
|
|||
function closeRecommendedGroupSizeModal(): CloseRecommendedGroupSizeModalActionType {
|
||||
return { type: 'CLOSE_RECOMMENDED_GROUP_SIZE_MODAL' };
|
||||
}
|
||||
function scrollToBottom(conversationId: string): ScrollToBpttomActionType {
|
||||
return {
|
||||
type: 'SCROLL_TO_BOTTOM',
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function scrollToMessage(
|
||||
conversationId: string,
|
||||
messageId: string
|
||||
|
@ -2454,6 +2472,9 @@ export function reducer(
|
|||
scrollToMessageCounter: existingConversation
|
||||
? existingConversation.scrollToMessageCounter + 1
|
||||
: 0,
|
||||
scrollToBottomCounter: existingConversation
|
||||
? existingConversation.scrollToBottomCounter + 1
|
||||
: 0,
|
||||
messageIds,
|
||||
metrics: {
|
||||
...metrics,
|
||||
|
@ -2533,6 +2554,28 @@ export function reducer(
|
|||
},
|
||||
};
|
||||
}
|
||||
if (action.type === 'SCROLL_TO_BOTTOM') {
|
||||
const { payload } = action;
|
||||
const { conversationId } = payload;
|
||||
const { messagesByConversation } = state;
|
||||
const existingConversation = messagesByConversation[conversationId];
|
||||
|
||||
if (!existingConversation) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
messagesByConversation: {
|
||||
...messagesByConversation,
|
||||
[conversationId]: {
|
||||
...existingConversation,
|
||||
scrollToBottomCounter: existingConversation.scrollToBottomCounter + 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'SCROLL_TO_MESSAGE') {
|
||||
const { payload } = action;
|
||||
const { conversationId, messageId } = payload;
|
||||
|
|
|
@ -824,6 +824,7 @@ export function _conversationMessagesSelector(
|
|||
messageIds,
|
||||
metrics,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToMessageId,
|
||||
scrollToMessageCounter,
|
||||
} = conversation;
|
||||
|
@ -862,7 +863,7 @@ export function _conversationMessagesSelector(
|
|||
isLoadingMessages,
|
||||
loadCountdownStart,
|
||||
items,
|
||||
isNearBottom,
|
||||
isNearBottom: isNearBottom || false,
|
||||
messageHeightChangeIndex:
|
||||
isNumber(messageHeightChangeIndex) && messageHeightChangeIndex >= 0
|
||||
? messageHeightChangeIndex
|
||||
|
@ -872,6 +873,7 @@ export function _conversationMessagesSelector(
|
|||
? oldestUnreadIndex
|
||||
: undefined,
|
||||
resetCounter,
|
||||
scrollToBottomCounter,
|
||||
scrollToIndex:
|
||||
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
|
||||
scrollToIndexCounter: scrollToMessageCounter,
|
||||
|
@ -907,10 +909,16 @@ export const getConversationMessagesSelector = createSelector(
|
|||
haveNewest: false,
|
||||
haveOldest: false,
|
||||
isLoadingMessages: false,
|
||||
isNearBottom: false,
|
||||
items: [],
|
||||
loadCountdownStart: undefined,
|
||||
messageHeightChangeIndex: undefined,
|
||||
oldestUnreadIndex: undefined,
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToIndex: undefined,
|
||||
scrollToIndexCounter: 0,
|
||||
totalUnread: 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { AttachmentType } from '../../types/Attachment';
|
|||
|
||||
export type SmartForwardMessageModalProps = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
conversationId: string;
|
||||
doForwardMessage: (
|
||||
selectedContacts: Array<string>,
|
||||
messageBody?: string,
|
||||
|
@ -40,6 +41,7 @@ const mapStateToProps = (
|
|||
): DataPropsType => {
|
||||
const {
|
||||
attachments,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
isSticker,
|
||||
messageBody,
|
||||
|
@ -56,6 +58,7 @@ const mapStateToProps = (
|
|||
return {
|
||||
attachments,
|
||||
candidateConversations,
|
||||
conversationId,
|
||||
doForwardMessage,
|
||||
i18n: getIntl(state),
|
||||
isSticker,
|
||||
|
|
|
@ -337,6 +337,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
totalUnread: 0,
|
||||
},
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToMessageCounter: 0,
|
||||
};
|
||||
}
|
||||
|
@ -839,6 +840,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
messageIds: [messageId],
|
||||
metrics: { totalUnread: 0 },
|
||||
resetCounter: 0,
|
||||
scrollToBottomCounter: 0,
|
||||
scrollToMessageCounter: 0,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1654,6 +1654,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
window.reduxStore,
|
||||
{
|
||||
attachments,
|
||||
conversationId: this.model.id,
|
||||
doForwardMessage: async (
|
||||
conversationIds: Array<string>,
|
||||
messageBody?: string,
|
||||
|
|
Loading…
Reference in a new issue