-
Notifications
You must be signed in to change notification settings - Fork 290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Jump to last message WPB-6518 #17386
base: dev
Are you sure you want to change the base?
Conversation
src/script/components/LastMessageTracker/LastMessageTracker.tsx
Outdated
Show resolved
Hide resolved
caec31b
to
691c0bc
Compare
4e632c1
to
36006ba
Compare
a07a09f
to
1672892
Compare
678de8a
to
21d2a75
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #17386 +/- ##
==========================================
+ Coverage 46.26% 46.29% +0.03%
==========================================
Files 756 758 +2
Lines 24859 24893 +34
Branches 5700 5710 +10
==========================================
+ Hits 11501 11525 +24
- Misses 11916 11927 +11
+ Partials 1442 1441 -1 |
@@ -239,7 +239,7 @@ export class ContentViewModel { | |||
exposeMessage: exposeMessageEntity, | |||
openFirstSelfMention = false, | |||
openNotificationSettings = false, | |||
} = options; | |||
} = options || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options are not supposed to be null | undefined, what is this change for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our TS config allowed me to use this function without providing this parameter and I got an exception. And actually, in many cases we provide just {}
, so I'd rather make options
optional and remove those {}
, WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be the preferred way to act on this issue. If it can be undefined, it needs to be typed as such 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -163,6 +167,7 @@ export class Conversation { | |||
public readonly receiptMode: ko.Observable<RECEIPT_MODE>; | |||
public readonly removed_from_conversation: ko.PureComputed<boolean>; | |||
public readonly roles: ko.Observable<Record<string, string>>; | |||
public readonly isLastMessageVisible: ko.Observable<boolean>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not belong in the low level entity. Should be a view-only thing.
It should leave in Conversation.tsx
only, I believe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was my idea too, but then we should pass it to the MessageList
to implement its updating, or pull all the updating logic into the Conversation
, chaining props of onVisible
and onVisibilityLost
. It was quite ugly implementation. I also thought that some of the props and methods of ConversationEntity
might not be in the perfect place (like initialMessage
), and maybe we need a ConversationView
service, WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we do need a cleaner architecture for those values, yes. But maybe it's not the correct PR to tackle this. Probably a follow up PR would be nice.
const jumpToLastMessage = () => { | ||
if (conversation) { | ||
// clean up anything like search result | ||
setHighlightedMessage(undefined); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels that this cleanup should happen somewhere in the useEffect
, when props and observables change, but I'm afraid to break something this way
); | ||
}; | ||
|
||
export interface JumpToLastMessageButtonProps extends HTMLProps<HTMLElement> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we agreed to keep this component inside of MessageList.tsx
as it contains positioning CSS and we didn't want to overcomplicate it by passing the custom CSS. It still should be a separate component with it's own state, as otherwise isLastMessageVisible
will make the entire MessageList
to re-render, unless we will implement memoisation of each element rendered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export
is for the test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think You can move it outside MessageList, and use - memo(Component) with validating props :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there were quite some issues with passing the css
, we moved it back and forth a few times already. I'd leave it as is for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
const [isLastMessageVisible, setIsLastMessageVisible] = useState(conversation.isLastMessageVisible()); | ||
|
||
useEffect(() => { | ||
const subscription = conversation.isLastMessageVisible.subscribe( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't use here useKoSubscribableChildren
as I need to add debouncing to the change of the state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do debounce with useCallback component, then You can use useKoSubscribableChildren :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do this smth like that:
const {isLastMessageVisible} = useKoSubscribableChildren(conversation, ['isLastMessageVisible']);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounced = useCallback(
debounce((value: boolean) => value, DEBOUNCE_TIME),
[],
);
if (!debounced(isLastMessageVisible)) {
return null;
}
return ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried it, but there is also one more thing - the initial value of isLastMessageVisible
should be set by the last fired value. Just waiting for the new event and debouncing makes it behave really weird. I suggest to keep my existing implementation
|
||
useEffect(() => { | ||
const subscription = conversation.isLastMessageVisible.subscribe( | ||
debounce(value => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
debouncing is needed for messages, sent by self
: at first the message from self gets mounted as not yet delivered, then it gets unmounted and mounted as received from the server. this makes the jump to the last message button blink. there is a complicated way to check if a message that is unmounted (its onVisibilityLost
was called) has not yet been delivered, but we should somehow distinguish whether it was unmounted and not hidden by scrolling. I think debouncing is better, while it does add a bit of noticeable delay even with 200 ms
@@ -25,6 +25,7 @@ import {viewportObserver} from 'Util/DOM/viewportObserver'; | |||
interface InViewportParams { | |||
onVisible: () => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the current implementation, this callback will be called when most of the element is visible. and we need it this way to mark the message as read. if I reuse this callback, the button "jump to the last message" will be disappearing slightly different from how it behaves on our android app - not when a bit of the last message appears, but when the big chunk of it is visible. @atomrc suggested we leave it as it is. the other option would be to implement one callback onPartialVisibilityChange(visible: boolean)
instead of onVisibilityLost()
and call it with true
differently than onVisible
(as onVisible
is used in many places, not only for messages)
0d40cd6
to
450a985
Compare
export const jumpToLastMessageButtonStyles: (mdBreakpoint: boolean) => CSSObject = (mdBreakpoint: boolean) => ({ | ||
position: 'absolute', | ||
bottom: mdBreakpoint ? '100px' : '56px', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do smth like that:
export const jumpToLastMessageButtonStyles: (mdBreakpoint: boolean) => CSSObject = (mdBreakpoint: boolean) => ({ | |
position: 'absolute', | |
bottom: mdBreakpoint ? '100px' : '56px', | |
export const jumpToLastMessageButtonStyles: (mdBreakpoint: boolean) => CSSObject = (mdBreakpoint: boolean) => ({ | |
position: 'absolute', | |
bottom: '56px', | |
'@media (max-width: @screen-md-min)': { | |
bottom: '100px', | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like media queries don't accept less variables, so I just hardcoded 768px
conversation: Conversation; | ||
} | ||
|
||
export const JumpToLastMessageButton: FC<JumpToLastMessageButtonProps> = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to pass FC<..> here, You have already typed on line 379 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
onClick={onGoToLastMessage} | ||
css={jumpToLastMessageButtonStyles(mdBreakpoint)} | ||
> | ||
<ChevronIcon css={{rotate: '90deg', height: 16, width: 16, path: {fill: '#0667C8'}}} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use variables for fill :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where should I add it? this is a completely new color AFAIK, we don't have it in our ui-kit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me check, a new color is always a red flag
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<ChevronIcon css={{rotate: '90deg', height: 16, width: 16, path: {fill: '#0667C8'}}} /> | |
<ChevronIcon css={{rotate: '90deg', height: 16, width: 16, path: {fill: 'var(--accent-color)'}}} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/script/entity/Conversation.ts
Outdated
export const isLastReceivedMessage = (messageEntity: Message, conversationEntity: Conversation): boolean => { | ||
return messageEntity.timestamp() >= conversationEntity.last_event_timestamp(); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not to use hasLastReceivedMessageLoaded, You have declared it in this Class ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because in this case, I need to identify if the given (rendered) message is the last message, to add to it onVisible
and onVisibilityLost
. but in many cases it can be replaced with hasLastReceivedMessageLoaded
- I'm planning to do the refactoring of it later, within the bug that I discovered. here I just wanted to move it out of React component, as a first step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So please move this from Class :) We have some utils/helpers for cases like this one :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done. but I think I will soon move it back as a method :)
…sage; Mark all the conversation as read on the "jump to last message" button click; Reload the conversation on the "jump to last message" click if the latest conversation messages were not load;
Add re-render of message list on click on button when last message is loaded;
Make isLastReceivedMessage independent function and use it for last message visibility; Fix tests;
…render; Add comments for jump to the last message function; Make options optional in `showConversation`; Fix CSS for the button and disable eslint for the WithConditionalCSSProp impert;
Make JumpToLastMessageButton part of MessageList file to not let it be used outside; Use debounce to avoid too often button visibility change; Make onVisibilityLost call on component unmount configurable;
Initial message is a static property that is read only when a conversation is loaded. Once loaded this value should not be read again and should not trigger re-rendering
Fix responsive positioning of the button;
93706c5
to
3f2f8cf
Compare
Move isLastReceivedMessage to utils; Move all CSS for button into the styles file;
Quality Gate passedIssues Measures |
Description
By the product/design team request, the user should jump to the very last message in the conversation, marking all the messages as read
Screenshots/Screencast (for UI changes)
Screen.Recording.2024-06-04.at.11.26.54.mov
Screen.Recording.2024-06-04.at.11.28.01.mov
Screen.Recording.2024-06-04.at.11.30.25.mov
Checklist