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',
direction: 'incoming',
timestamp: Date.now(),
authorPhoneNumber: '(202) 555-2001',
authorColor: 'green',
author: {
phoneNumber: '(202) 555-2001',
color: 'green',
},
text: '🔥',
},
},
@ -47,7 +49,9 @@ const items: Record<string, TimelineItemType> = {
conversationType: 'group',
direction: 'incoming',
timestamp: Date.now(),
authorColor: 'green',
author: {
color: 'green',
},
text: 'Hello there from the new world! http://somewhere.com',
},
},
@ -70,7 +74,9 @@ const items: Record<string, TimelineItemType> = {
collapseMetadata: true,
direction: 'incoming',
timestamp: Date.now(),
authorColor: 'red',
author: {
color: 'red',
},
text: 'Hello there from the new world!',
},
},
@ -154,7 +160,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing',
timestamp: Date.now(),
status: 'sent',
authorColor: 'pink',
author: {
color: 'pink',
},
text: '🔥',
},
},
@ -165,7 +173,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing',
timestamp: Date.now(),
status: 'read',
authorColor: 'pink',
author: {
color: 'pink',
},
text: 'Hello there from the new world! http://somewhere.com',
},
},
@ -187,7 +197,9 @@ const items: Record<string, TimelineItemType> = {
direction: 'outgoing',
status: 'sent',
timestamp: Date.now(),
authorColor: 'blue',
author: {
color: 'blue',
},
text:
'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),
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),
resetCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
@ -367,6 +386,40 @@ story.add('Oldest and Newest', () => {
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)', () => {
const props = createProps({
items: [],
@ -400,23 +453,6 @@ story.add('Typing Indicator', () => {
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', () => {
const props = createProps({
invitedContactsForNewlyCreatedGroup: [

View file

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

View file

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

View file

@ -1068,8 +1068,10 @@ Whisper.ConversationView = Whisper.View.extend({
return finish;
},
async loadAndScroll(messageId: any, options: any) {
const { disableScroll } = options || {};
async loadAndScroll(
messageId: string,
options?: { disableScroll?: boolean }
) {
const {
messagesReset,
setMessagesLoading,
@ -1110,7 +1112,8 @@ Whisper.ConversationView = Whisper.View.extend({
const cleaned = await this.cleanModels(all);
this.model.messageCollection.reset(cleaned);
const scrollToMessageId = disableScroll ? undefined : messageId;
const scrollToMessageId =
options && options.disableScroll ? undefined : messageId;
messagesReset(
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 {
messagesReset,
setMessagesLoading,
} = window.reduxActions.conversations;
const conversationId = this.model.id;
const { model }: { model: ConversationModel } = this;
const conversationId = model.id;
setMessagesLoading(conversationId, true);
const finish = this.setInProgressFetch();
@ -1158,6 +1166,15 @@ Whisper.ConversationView = Whisper.View.extend({
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) {
this.loadAndScroll(metrics.oldestUnread.id, {
disableScroll: !setFocus,
@ -1171,7 +1188,12 @@ Whisper.ConversationView = Whisper.View.extend({
});
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 =
setFocus && metrics.newest ? metrics.newest.id : undefined;
@ -1183,7 +1205,7 @@ Whisper.ConversationView = Whisper.View.extend({
const unboundedFetch = true;
messagesReset(
conversationId,
cleaned.map((model: any) => model.getReduxData()),
cleaned.map((messageModel: any) => messageModel.getReduxData()),
metrics,
scrollToMessageId,
unboundedFetch