Message Requests: Always open to top of conversation

This commit is contained in:
Scott Nonnenberg 2021-04-30 15:59:37 -07:00 committed by GitHub
parent fe772af251
commit cf1eb77ed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 47 deletions

View file

@ -35,8 +35,10 @@ const items: Record<string, TimelineItemType> = {
id: 'id-1', id: 'id-1',
direction: 'incoming', direction: 'incoming',
timestamp: Date.now(), timestamp: Date.now(),
authorPhoneNumber: '(202) 555-2001', author: {
authorColor: 'green', phoneNumber: '(202) 555-2001',
color: 'green',
},
text: '🔥', text: '🔥',
}, },
}, },
@ -47,7 +49,9 @@ const items: Record<string, TimelineItemType> = {
conversationType: 'group', conversationType: 'group',
direction: 'incoming', direction: 'incoming',
timestamp: Date.now(), timestamp: Date.now(),
authorColor: 'green', author: {
color: 'green',
},
text: 'Hello there from the new world! http://somewhere.com', text: 'Hello there from the new world! http://somewhere.com',
}, },
}, },
@ -70,7 +74,9 @@ const items: Record<string, TimelineItemType> = {
collapseMetadata: true, collapseMetadata: true,
direction: 'incoming', direction: 'incoming',
timestamp: Date.now(), timestamp: Date.now(),
authorColor: 'red', author: {
color: 'red',
},
text: 'Hello there from the new world!', text: 'Hello there from the new world!',
}, },
}, },
@ -154,7 +160,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing', direction: 'outgoing',
timestamp: Date.now(), timestamp: Date.now(),
status: 'sent', status: 'sent',
authorColor: 'pink', author: {
color: 'pink',
},
text: '🔥', text: '🔥',
}, },
}, },
@ -165,7 +173,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing', direction: 'outgoing',
timestamp: Date.now(), timestamp: Date.now(),
status: 'read', status: 'read',
authorColor: 'pink', author: {
color: 'pink',
},
text: 'Hello there from the new world! http://somewhere.com', text: 'Hello there from the new world! http://somewhere.com',
}, },
}, },
@ -187,7 +197,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing', direction: 'outgoing',
status: 'sent', status: 'sent',
timestamp: Date.now(), timestamp: Date.now(),
authorColor: 'blue', author: {
color: 'blue',
},
text: text:
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.', 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}, },
@ -334,7 +346,14 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false), haveNewest: boolean('haveNewest', overrideProps.haveNewest !== false),
haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false), haveOldest: boolean('haveOldest', overrideProps.haveOldest !== false),
isLoadingMessages: false, isIncomingMessageRequest: boolean(
'isIncomingMessageRequest',
overrideProps.isIncomingMessageRequest === true
),
isLoadingMessages: boolean(
'isLoadingMessages',
overrideProps.isLoadingMessages === false
),
items: overrideProps.items || Object.keys(items), items: overrideProps.items || Object.keys(items),
resetCounter: 0, resetCounter: 0,
scrollToIndex: overrideProps.scrollToIndex, scrollToIndex: overrideProps.scrollToIndex,
@ -367,6 +386,40 @@ story.add('Oldest and Newest', () => {
return <Timeline {...props} />; return <Timeline {...props} />;
}); });
story.add('With active message request', () => {
const props = createProps({
isIncomingMessageRequest: true,
});
return <Timeline {...props} />;
});
story.add('Without Newest Message', () => {
const props = createProps({
haveNewest: false,
});
return <Timeline {...props} />;
});
story.add('Without newest message, active message request', () => {
const props = createProps({
haveOldest: false,
isIncomingMessageRequest: true,
});
return <Timeline {...props} />;
});
story.add('Without Oldest Message', () => {
const props = createProps({
haveOldest: false,
scrollToIndex: -1,
});
return <Timeline {...props} />;
});
story.add('Empty (just hero)', () => { story.add('Empty (just hero)', () => {
const props = createProps({ const props = createProps({
items: [], items: [],
@ -400,23 +453,6 @@ story.add('Typing Indicator', () => {
return <Timeline {...props} />; return <Timeline {...props} />;
}); });
story.add('Without Newest Message', () => {
const props = createProps({
haveNewest: false,
});
return <Timeline {...props} />;
});
story.add('Without Oldest Message', () => {
const props = createProps({
haveOldest: false,
scrollToIndex: -1,
});
return <Timeline {...props} />;
});
story.add('With invited contacts for a newly-created group', () => { story.add('With invited contacts for a newly-created group', () => {
const props = createProps({ const props = createProps({
invitedContactsForNewlyCreatedGroup: [ invitedContactsForNewlyCreatedGroup: [

View file

@ -57,9 +57,10 @@ export type PropsDataType = {
type PropsHousekeepingType = { type PropsHousekeepingType = {
id: string; id: string;
unreadCount?: number;
typingContact?: unknown;
isGroupV1AndDisabled?: boolean; isGroupV1AndDisabled?: boolean;
isIncomingMessageRequest: boolean;
typingContact?: unknown;
unreadCount?: number;
selectedMessageId?: string; selectedMessageId?: string;
invitedContactsForNewlyCreatedGroup: Array<ConversationType>; invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
@ -198,11 +199,16 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
const { scrollToIndex } = this.props; const { scrollToIndex, isIncomingMessageRequest } = this.props;
const oneTimeScrollRow = this.getLastSeenIndicatorRow(); const oneTimeScrollRow = isIncomingMessageRequest
? undefined
: this.getLastSeenIndicatorRow();
// We only stick to the bottom if this is not an incoming message request.
const atBottom = !isIncomingMessageRequest;
this.state = { this.state = {
atBottom: true, atBottom,
atTop: false, atTop: false,
oneTimeScrollRow, oneTimeScrollRow,
propScrollToIndex: scrollToIndex, propScrollToIndex: scrollToIndex,
@ -364,6 +370,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
haveNewest, haveNewest,
haveOldest, haveOldest,
id, id,
isIncomingMessageRequest,
setIsNearBottom, setIsNearBottom,
setLoadCountdownStart, setLoadCountdownStart,
} = this.props; } = this.props;
@ -386,8 +393,12 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
scrollHeight - clientHeight - scrollTop scrollHeight - clientHeight - scrollTop
); );
const atBottom = // If there's an active message request, we won't stick to the bottom of the
haveNewest && this.offsetFromBottom <= AT_BOTTOM_THRESHOLD; // conversation as new messages come in.
const atBottom = isIncomingMessageRequest
? false
: haveNewest && this.offsetFromBottom <= AT_BOTTOM_THRESHOLD;
const isNearBottom = const isNearBottom =
haveNewest && this.offsetFromBottom <= NEAR_BOTTOM_THRESHOLD; haveNewest && this.offsetFromBottom <= NEAR_BOTTOM_THRESHOLD;
const atTop = scrollTop <= AT_TOP_THRESHOLD; const atTop = scrollTop <= AT_TOP_THRESHOLD;
@ -773,10 +784,12 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
selectMessage(lastMessageId, id); selectMessage(lastMessageId, id);
} }
const oneTimeScrollRow =
items && items.length > 0 ? items.length - 1 : undefined;
this.setState({ this.setState({
propScrollToIndex: undefined, propScrollToIndex: undefined,
oneTimeScrollRow: undefined, oneTimeScrollRow,
atBottom: true,
}); });
}; };
@ -850,8 +863,9 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
prevState: Readonly<StateType> prevState: Readonly<StateType>
): void { ): void {
const { const {
id,
clearChangedMessages, clearChangedMessages,
id,
isIncomingMessageRequest,
items, items,
messageHeightChangeIndex, messageHeightChangeIndex,
oldestUnreadIndex, oldestUnreadIndex,
@ -885,12 +899,17 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
this.resize(); this.resize();
} }
const oneTimeScrollRow = this.getLastSeenIndicatorRow(); // We want to come in at the top of the conversation if it's a message request
const oneTimeScrollRow = isIncomingMessageRequest
? undefined
: this.getLastSeenIndicatorRow();
const atBottom = !isIncomingMessageRequest;
// TODO: DESKTOP-688 // TODO: DESKTOP-688
// eslint-disable-next-line react/no-did-update-set-state // eslint-disable-next-line react/no-did-update-set-state
this.setState({ this.setState({
oneTimeScrollRow, oneTimeScrollRow,
atBottom: true, atBottom,
propScrollToIndex: scrollToIndex, propScrollToIndex: scrollToIndex,
prevPropScrollToIndex: scrollToIndex, prevPropScrollToIndex: scrollToIndex,
}); });
@ -1009,13 +1028,13 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state; const { oneTimeScrollRow, atBottom, propScrollToIndex } = this.state;
const rowCount = this.getRowCount(); const rowCount = this.getRowCount();
const targetMessage = isNumber(propScrollToIndex) const targetMessageRow = isNumber(propScrollToIndex)
? this.fromItemIndexToRow(propScrollToIndex) ? this.fromItemIndexToRow(propScrollToIndex)
: undefined; : undefined;
const scrollToBottom = atBottom ? rowCount - 1 : undefined; const scrollToBottom = atBottom ? rowCount - 1 : undefined;
if (isNumber(targetMessage)) { if (isNumber(targetMessageRow)) {
return targetMessage; return targetMessageRow;
} }
if (isNumber(oneTimeScrollRow)) { if (isNumber(oneTimeScrollRow)) {

View file

@ -157,6 +157,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
'typingContact', 'typingContact',
'isGroupV1AndDisabled', 'isGroupV1AndDisabled',
]), ]),
isIncomingMessageRequest: Boolean(
conversation.messageRequestsEnabled &&
!conversation.acceptedMessageRequest
),
...conversationMessages, ...conversationMessages,
invitedContactsForNewlyCreatedGroup: getInvitedContactsForNewlyCreatedGroup( invitedContactsForNewlyCreatedGroup: getInvitedContactsForNewlyCreatedGroup(
state state

View file

@ -1068,8 +1068,10 @@ Whisper.ConversationView = Whisper.View.extend({
return finish; return finish;
}, },
async loadAndScroll(messageId: any, options: any) { async loadAndScroll(
const { disableScroll } = options || {}; messageId: string,
options?: { disableScroll?: boolean }
) {
const { const {
messagesReset, messagesReset,
setMessagesLoading, setMessagesLoading,
@ -1110,7 +1112,8 @@ Whisper.ConversationView = Whisper.View.extend({
const cleaned = await this.cleanModels(all); const cleaned = await this.cleanModels(all);
this.model.messageCollection.reset(cleaned); this.model.messageCollection.reset(cleaned);
const scrollToMessageId = disableScroll ? undefined : messageId; const scrollToMessageId =
options && options.disableScroll ? undefined : messageId;
messagesReset( messagesReset(
conversationId, conversationId,
@ -1126,12 +1129,17 @@ Whisper.ConversationView = Whisper.View.extend({
} }
}, },
async loadNewestMessages(newestMessageId: any, setFocus: any) { async loadNewestMessages(
newestMessageId: string | undefined,
setFocus: boolean | undefined
): Promise<void> {
const { const {
messagesReset, messagesReset,
setMessagesLoading, setMessagesLoading,
} = window.reduxActions.conversations; } = window.reduxActions.conversations;
const conversationId = this.model.id; const { model }: { model: ConversationModel } = this;
const conversationId = model.id;
setMessagesLoading(conversationId, true); setMessagesLoading(conversationId, true);
const finish = this.setInProgressFetch(); const finish = this.setInProgressFetch();
@ -1158,6 +1166,15 @@ Whisper.ConversationView = Whisper.View.extend({
const metrics = await getMessageMetricsForConversation(conversationId); const metrics = await getMessageMetricsForConversation(conversationId);
// If this is a message request that has not yet been accepted, we always show the
// oldest messages, to ensure that the ConversationHero is shown. We don't want to
// scroll directly to the oldest message, because that could scroll the hero off
// the screen.
if (!newestMessageId && !model.getAccepted() && metrics.oldest) {
this.loadAndScroll(metrics.oldest.id, { disableScroll: true });
return;
}
if (scrollToLatestUnread && metrics.oldestUnread) { if (scrollToLatestUnread && metrics.oldestUnread) {
this.loadAndScroll(metrics.oldestUnread.id, { this.loadAndScroll(metrics.oldestUnread.id, {
disableScroll: !setFocus, disableScroll: !setFocus,
@ -1171,7 +1188,12 @@ Whisper.ConversationView = Whisper.View.extend({
}); });
const cleaned = await this.cleanModels(messages); const cleaned = await this.cleanModels(messages);
this.model.messageCollection.reset(cleaned); assert(
model.messageCollection,
'loadNewestMessages: model must have messageCollection'
);
model.messageCollection.reset(cleaned);
const scrollToMessageId = const scrollToMessageId =
setFocus && metrics.newest ? metrics.newest.id : undefined; setFocus && metrics.newest ? metrics.newest.id : undefined;
@ -1183,7 +1205,7 @@ Whisper.ConversationView = Whisper.View.extend({
const unboundedFetch = true; const unboundedFetch = true;
messagesReset( messagesReset(
conversationId, conversationId,
cleaned.map((model: any) => model.getReduxData()), cleaned.map((messageModel: any) => messageModel.getReduxData()),
metrics, metrics,
scrollToMessageId, scrollToMessageId,
unboundedFetch unboundedFetch