Message Requests: Always open to top of conversation
This commit is contained in:
parent
fe772af251
commit
cf1eb77ed8
4 changed files with 128 additions and 47 deletions
|
@ -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: [
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -157,6 +157,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
'typingContact',
|
||||
'isGroupV1AndDisabled',
|
||||
]),
|
||||
isIncomingMessageRequest: Boolean(
|
||||
conversation.messageRequestsEnabled &&
|
||||
!conversation.acceptedMessageRequest
|
||||
),
|
||||
...conversationMessages,
|
||||
invitedContactsForNewlyCreatedGroup: getInvitedContactsForNewlyCreatedGroup(
|
||||
state
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue