Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32089034
chat-message-list.react.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
13 KB
Referenced Files
None
Subscribers
None
chat-message-list.react.js
View Options
// @flow
import
classNames
from
'classnames'
;
import
{
detect
as
detectBrowser
}
from
'detect-browser'
;
import
invariant
from
'invariant'
;
import
*
as
React
from
'react'
;
import
{
useDrop
}
from
'react-dnd'
;
import
{
NativeTypes
}
from
'react-dnd-html5-backend'
;
import
{
fetchMessagesBeforeCursorActionTypes
,
fetchMessagesBeforeCursor
,
fetchMostRecentMessagesActionTypes
,
}
from
'lib/actions/message-actions'
;
import
{
registerFetchKey
}
from
'lib/reducers/loading-reducer'
;
import
{
type
ChatMessageItem
,
useMessageListData
,
}
from
'lib/selectors/chat-selectors'
;
import
{
threadInfoSelector
}
from
'lib/selectors/thread-selectors'
;
import
{
messageKey
}
from
'lib/shared/message-utils'
;
import
{
useWatchThread
,
useCurrentThreadInfo
}
from
'lib/shared/thread-utils'
;
import
type
{
FetchMessageInfosPayload
}
from
'lib/types/message-types'
;
import
{
type
ThreadInfo
}
from
'lib/types/thread-types'
;
import
{
type
DispatchActionPromise
,
useServerCall
,
useDispatchActionPromise
,
}
from
'lib/utils/action-utils'
;
import
{
type
InputState
,
InputStateContext
}
from
'../input/input-state'
;
import
LoadingIndicator
from
'../loading-indicator.react'
;
import
{
useTextMessageRulesFunc
}
from
'../markdown/rules.react'
;
import
{
useSelector
}
from
'../redux/redux-utils'
;
import
ChatInputBar
from
'./chat-input-bar.react'
;
import
css
from
'./chat-message-list.css'
;
import
{
MessageListContext
}
from
'./message-list-types'
;
import
MessageTimestampTooltip
from
'./message-timestamp-tooltip.react'
;
import
Message
from
'./message.react'
;
import
type
{
OnMessagePositionWithContainerInfo
,
MessagePositionInfo
,
}
from
'./position-types'
;
type
BaseProps
=
{
|
+
setModal
:
(
modal
:
?
React
.
Node
)
=>
void
,
|
};
type
PassedProps
=
{
|
...
BaseProps
,
// Redux state
+
activeChatThreadID
:
?
string
,
+
threadInfo
:
?
ThreadInfo
,
+
messageListData
:
?
$ReadOnlyArray
<
ChatMessageItem
>
,
+
startReached
:
boolean
,
+
timeZone
:
?
string
,
+
supportsReverseFlex
:
boolean
,
// Redux dispatch functions
+
dispatchActionPromise
:
DispatchActionPromise
,
// async functions that hit server APIs
+
fetchMessagesBeforeCursor
:
(
threadID
:
string
,
beforeMessageID
:
string
,
)
=>
Promise
<
FetchMessageInfosPayload
>
,
// withInputState
+
inputState
:
?
InputState
,
|
};
type
ReactDnDProps
=
{
|
isActive
:
boolean
,
connectDropTarget
:
(
node
:
React
.
Node
)
=>
React
.
Node
,
|
};
type
Props
=
{
|
...
PassedProps
,
...
ReactDnDProps
,
|
};
type
State
=
{
|
+
mouseOverMessagePosition
:
?
OnMessagePositionWithContainerInfo
,
|
};
type
Snapshot
=
{
|
+
scrollTop
:
number
,
+
scrollHeight
:
number
,
|
};
class
ChatMessageList
extends
React
.
PureComponent
<
Props
,
State
>
{
state
:
State
=
{
mouseOverMessagePosition
:
null
,
};
container
:
?
HTMLDivElement
;
messageContainer
:
?
HTMLDivElement
;
loadingFromScroll
=
false
;
componentDidMount
()
{
this
.
scrollToBottom
();
}
getSnapshotBeforeUpdate
(
prevProps
:
Props
)
{
if
(
ChatMessageList
.
hasNewMessage
(
this
.
props
,
prevProps
)
&&
this
.
messageContainer
)
{
const
{
scrollTop
,
scrollHeight
}
=
this
.
messageContainer
;
return
{
scrollTop
,
scrollHeight
};
}
return
null
;
}
static
hasNewMessage
(
props
:
Props
,
prevProps
:
Props
)
{
const
{
messageListData
}
=
props
;
if
(
!
messageListData
||
messageListData
.
length
===
0
)
{
return
false
;
}
const
prevMessageListData
=
prevProps
.
messageListData
;
if
(
!
prevMessageListData
||
prevMessageListData
.
length
===
0
)
{
return
true
;
}
return
(
ChatMessageList
.
keyExtractor
(
prevMessageListData
[
0
])
!==
ChatMessageList
.
keyExtractor
(
messageListData
[
0
])
);
}
componentDidUpdate
(
prevProps
:
Props
,
prevState
:
State
,
snapshot
:
?
Snapshot
)
{
const
{
messageListData
}
=
this
.
props
;
const
prevMessageListData
=
prevProps
.
messageListData
;
if
(
this
.
loadingFromScroll
&&
messageListData
&&
(
!
prevMessageListData
||
messageListData
.
length
>
prevMessageListData
.
length
||
this
.
props
.
startReached
)
)
{
this
.
loadingFromScroll
=
false
;
}
const
{
messageContainer
}
=
this
;
if
(
messageContainer
&&
prevMessageListData
!==
messageListData
)
{
this
.
onScroll
();
}
// We'll scroll to the bottom if the user was already scrolled to the bottom
// before the new message, or if the new message was composed locally
const
hasNewMessage
=
ChatMessageList
.
hasNewMessage
(
this
.
props
,
prevProps
);
if
(
this
.
props
.
activeChatThreadID
!==
prevProps
.
activeChatThreadID
||
(
hasNewMessage
&&
messageListData
&&
messageListData
[
0
].
itemType
===
'message'
&&
messageListData
[
0
].
messageInfo
.
localID
)
||
(
hasNewMessage
&&
snapshot
&&
Math
.
abs
(
snapshot
.
scrollTop
)
<=
1
)
)
{
this
.
scrollToBottom
();
}
else
if
(
hasNewMessage
&&
messageContainer
&&
snapshot
)
{
const
{
scrollTop
,
scrollHeight
}
=
messageContainer
;
if
(
scrollHeight
>
snapshot
.
scrollHeight
&&
scrollTop
===
snapshot
.
scrollTop
)
{
const
newHeight
=
scrollHeight
-
snapshot
.
scrollHeight
;
const
newScrollTop
=
Math
.
abs
(
scrollTop
)
+
newHeight
;
if
(
this
.
props
.
supportsReverseFlex
)
{
messageContainer
.
scrollTop
=
-
1
*
newScrollTop
;
}
else
{
messageContainer
.
scrollTop
=
newScrollTop
;
}
}
}
}
scrollToBottom
()
{
if
(
this
.
messageContainer
)
{
this
.
messageContainer
.
scrollTop
=
0
;
}
}
static
keyExtractor
(
item
:
ChatMessageItem
)
{
if
(
item
.
itemType
===
'loader'
)
{
return
'loader'
;
}
return
messageKey
(
item
.
messageInfo
);
}
renderItem
=
(
item
)
=>
{
if
(
item
.
itemType
===
'loader'
)
{
return
(
<
div
key
=
"loader"
className
=
{
css
.
loading
}
>
<
LoadingIndicator
status
=
"loading"
size
=
"large"
color
=
"black"
/>
<
/div>
);
}
const
{
threadInfo
,
setModal
}
=
this
.
props
;
invariant
(
threadInfo
,
'ThreadInfo should be set if messageListData is'
);
return
(
<
Message
item
=
{
item
}
threadInfo
=
{
threadInfo
}
setMouseOverMessagePosition
=
{
this
.
setMouseOverMessagePosition
}
mouseOverMessagePosition
=
{
this
.
state
.
mouseOverMessagePosition
}
setModal
=
{
setModal
}
timeZone
=
{
this
.
props
.
timeZone
}
key
=
{
ChatMessageList
.
keyExtractor
(
item
)}
/>
);
};
setMouseOverMessagePosition
=
(
messagePositionInfo
:
MessagePositionInfo
)
=>
{
if
(
!
this
.
messageContainer
)
{
return
;
}
if
(
messagePositionInfo
.
type
===
'off'
)
{
this
.
setState
({
mouseOverMessagePosition
:
null
});
return
;
}
const
{
top
:
containerTop
,
bottom
:
containerBottom
,
left
:
containerLeft
,
right
:
containerRight
,
height
:
containerHeight
,
width
:
containerWidth
,
}
=
this
.
messageContainer
.
getBoundingClientRect
();
const
mouseOverMessagePosition
=
{
...
messagePositionInfo
,
messagePosition
:
{
...
messagePositionInfo
.
messagePosition
,
top
:
messagePositionInfo
.
messagePosition
.
top
-
containerTop
,
bottom
:
messagePositionInfo
.
messagePosition
.
bottom
-
containerTop
,
left
:
messagePositionInfo
.
messagePosition
.
left
-
containerLeft
,
right
:
messagePositionInfo
.
messagePosition
.
right
-
containerLeft
,
},
containerPosition
:
{
top
:
containerTop
,
bottom
:
containerBottom
,
left
:
containerLeft
,
right
:
containerRight
,
height
:
containerHeight
,
width
:
containerWidth
,
},
};
this
.
setState
({
mouseOverMessagePosition
});
};
render
()
{
const
{
messageListData
,
threadInfo
,
inputState
,
connectDropTarget
,
isActive
,
}
=
this
.
props
;
if
(
!
messageListData
)
{
return
<
div
className
=
{
css
.
container
}
/>
;
}
invariant
(
threadInfo
,
'ThreadInfo should be set if messageListData is'
);
invariant
(
inputState
,
'InputState should be set'
);
const
messages
=
messageListData
.
map
(
this
.
renderItem
);
const
containerStyle
=
classNames
({
[
css
.
container
]
:
true
,
[
css
.
activeContainer
]
:
isActive
,
});
let
tooltip
;
if
(
this
.
state
.
mouseOverMessagePosition
)
{
const
messagePositionInfo
=
this
.
state
.
mouseOverMessagePosition
;
tooltip
=
(
<
MessageTimestampTooltip
messagePositionInfo
=
{
messagePositionInfo
}
timeZone
=
{
this
.
props
.
timeZone
}
/>
);
}
const
messageContainerStyle
=
classNames
({
[
css
.
messageContainer
]
:
true
,
[
css
.
mirroredMessageContainer
]
:
!
this
.
props
.
supportsReverseFlex
,
});
return
connectDropTarget
(
<
div
className
=
{
containerStyle
}
ref
=
{
this
.
containerRef
}
>
<
div
className
=
{
css
.
outerMessageContainer
}
>
<
div
className
=
{
messageContainerStyle
}
ref
=
{
this
.
messageContainerRef
}
>
{
messages
}
<
/div>
{
tooltip
}
<
/div>
<
ChatInputBar
threadInfo
=
{
threadInfo
}
inputState
=
{
inputState
}
/>
<
/div>,
);
}
containerRef
=
(
container
:
?
HTMLDivElement
)
=>
{
if
(
container
)
{
container
.
addEventListener
(
'paste'
,
this
.
onPaste
);
}
this
.
container
=
container
;
};
onPaste
=
(
e
:
ClipboardEvent
)
=>
{
const
{
inputState
}
=
this
.
props
;
if
(
!
inputState
)
{
return
;
}
const
{
clipboardData
}
=
e
;
if
(
!
clipboardData
)
{
return
;
}
const
{
files
}
=
clipboardData
;
if
(
files
.
length
===
0
)
{
return
;
}
e
.
preventDefault
();
inputState
.
appendFiles
([...
files
]);
};
messageContainerRef
=
(
messageContainer
:
?
HTMLDivElement
)
=>
{
this
.
messageContainer
=
messageContainer
;
// In case we already have all the most recent messages,
// but they're not enough
this
.
possiblyLoadMoreMessages
();
if
(
messageContainer
)
{
messageContainer
.
addEventListener
(
'scroll'
,
this
.
onScroll
);
}
};
onScroll
=
()
=>
{
if
(
!
this
.
messageContainer
)
{
return
;
}
if
(
this
.
state
.
mouseOverMessagePosition
)
{
this
.
setState
({
mouseOverMessagePosition
:
null
});
}
this
.
possiblyLoadMoreMessages
();
};
possiblyLoadMoreMessages
()
{
if
(
!
this
.
messageContainer
)
{
return
;
}
const
{
scrollTop
,
scrollHeight
,
clientHeight
}
=
this
.
messageContainer
;
if
(
this
.
props
.
startReached
||
Math
.
abs
(
scrollTop
)
+
clientHeight
+
55
<
scrollHeight
)
{
return
;
}
const
oldestMessageServerID
=
this
.
oldestMessageServerID
();
if
(
!
oldestMessageServerID
)
{
return
;
}
if
(
this
.
loadingFromScroll
)
{
return
;
}
this
.
loadingFromScroll
=
true
;
const
threadID
=
this
.
props
.
activeChatThreadID
;
invariant
(
threadID
,
'should be set'
);
this
.
props
.
dispatchActionPromise
(
fetchMessagesBeforeCursorActionTypes
,
this
.
props
.
fetchMessagesBeforeCursor
(
threadID
,
oldestMessageServerID
),
);
}
oldestMessageServerID
()
:
?
string
{
const
data
=
this
.
props
.
messageListData
;
invariant
(
data
,
'should be set'
);
for
(
let
i
=
data
.
length
-
1
;
i
>=
0
;
i
--
)
{
if
(
data
[
i
].
itemType
===
'message'
&&
data
[
i
].
messageInfo
.
id
)
{
return
data
[
i
].
messageInfo
.
id
;
}
}
return
null
;
}
}
registerFetchKey
(
fetchMessagesBeforeCursorActionTypes
);
registerFetchKey
(
fetchMostRecentMessagesActionTypes
);
export
default
React
.
memo
<
BaseProps
>
(
function
ConnectedChatMessageList
(
props
:
BaseProps
,
)
{
const
userAgent
=
useSelector
((
state
)
=>
state
.
userAgent
);
const
supportsReverseFlex
=
React
.
useMemo
(()
=>
{
const
browser
=
detectBrowser
(
userAgent
);
return
(
!
browser
||
browser
.
name
!==
'firefox'
||
parseInt
(
browser
.
version
)
>=
81
);
},
[
userAgent
]);
const
timeZone
=
useSelector
((
state
)
=>
state
.
timeZone
);
const
activeChatThreadID
=
useSelector
(
(
state
)
=>
state
.
navInfo
.
activeChatThreadID
,
);
const
baseThreadInfo
=
useSelector
((
state
)
=>
{
const
activeID
=
state
.
navInfo
.
activeChatThreadID
;
if
(
!
activeID
)
{
return
null
;
}
return
threadInfoSelector
(
state
)[
activeID
]
??
state
.
navInfo
.
pendingThread
;
});
const
sourceMessageID
=
useSelector
((
state
)
=>
state
.
navInfo
.
sourceMessageID
);
const
threadInfo
=
useCurrentThreadInfo
({
baseThreadInfo
,
searching
:
false
,
userInfoInputArray
:
[],
});
const
messageListData
=
useMessageListData
({
sourceMessageID
,
searching
:
false
,
userInfoInputArray
:
[],
threadInfo
,
});
const
startReached
=
useSelector
((
state
)
=>
{
const
activeID
=
state
.
navInfo
.
activeChatThreadID
;
if
(
!
activeID
)
{
return
null
;
}
if
(
state
.
navInfo
.
pendingThread
)
{
return
true
;
}
const
threadMessageInfo
=
state
.
messageStore
.
threads
[
activeID
];
if
(
!
threadMessageInfo
)
{
return
null
;
}
return
threadMessageInfo
.
startReached
;
});
const
dispatchActionPromise
=
useDispatchActionPromise
();
const
callFetchMessagesBeforeCursor
=
useServerCall
(
fetchMessagesBeforeCursor
,
);
const
inputState
=
React
.
useContext
(
InputStateContext
);
const
[
dndProps
,
connectDropTarget
]
=
useDrop
({
accept
:
NativeTypes
.
FILE
,
drop
:
(
item
)
=>
{
const
{
files
}
=
item
;
if
(
inputState
&&
files
.
length
>
0
)
{
inputState
.
appendFiles
(
files
);
}
},
collect
:
(
monitor
)
=>
({
isActive
:
monitor
.
isOver
()
&&
monitor
.
canDrop
(),
}),
});
const
getTextMessageMarkdownRules
=
useTextMessageRulesFunc
(
threadInfo
?
.
id
);
const
messageListContext
=
React
.
useMemo
(()
=>
{
if
(
!
getTextMessageMarkdownRules
)
{
return
undefined
;
}
return
{
getTextMessageMarkdownRules
};
},
[
getTextMessageMarkdownRules
]);
useWatchThread
(
threadInfo
);
return
(
<
MessageListContext
.
Provider
value
=
{
messageListContext
}
>
<
ChatMessageList
{...
props
}
activeChatThreadID
=
{
activeChatThreadID
}
threadInfo
=
{
threadInfo
}
messageListData
=
{
messageListData
}
startReached
=
{
startReached
}
timeZone
=
{
timeZone
}
supportsReverseFlex
=
{
supportsReverseFlex
}
inputState
=
{
inputState
}
dispatchActionPromise
=
{
dispatchActionPromise
}
fetchMessagesBeforeCursor
=
{
callFetchMessagesBeforeCursor
}
{...
dndProps
}
connectDropTarget
=
{
connectDropTarget
}
/>
<
/MessageListContext.Provider>
);
});
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Sun, Dec 7, 7:55 AM (7 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5834102
Default Alt Text
chat-message-list.react.js (13 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment