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',
|
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: [
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue