mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/composer: add support for rich drafts
This commit is contained in:
parent
b37e4644b7
commit
24342a5dce
5 changed files with 115 additions and 72 deletions
|
@ -24,9 +24,12 @@ export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRoomEvent(room: RoomStateStore, eventID: EventID): MemDBEvent | null {
|
const noopSubscribe = () => () => {}
|
||||||
|
const returnNull = () => null
|
||||||
|
|
||||||
|
export function useRoomEvent(room: RoomStateStore, eventID: EventID | null): MemDBEvent | null {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
room.getEventSubscriber(eventID).subscribe,
|
eventID ? room.getEventSubscriber(eventID).subscribe : noopSubscribe,
|
||||||
() => room.eventsByID.get(eventID) ?? null,
|
eventID ? (() => room.eventsByID.get(eventID) ?? null) : returnNull,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { use, useCallback, useLayoutEffect, useRef, useState } from "react"
|
import React, { use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react"
|
||||||
import { ScaleLoader } from "react-spinners"
|
import { ScaleLoader } from "react-spinners"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore, useRoomEvent } from "@/api/statestore"
|
||||||
import { MediaMessageEventContent, MemDBEvent, Mentions, RoomID } from "@/api/types"
|
import { EventID, MediaMessageEventContent, Mentions, RoomID } from "@/api/types"
|
||||||
import { ClientContext } from "./ClientContext.ts"
|
import { ClientContext } from "./ClientContext.ts"
|
||||||
import { ReplyBody } from "./timeline/ReplyBody.tsx"
|
import { ReplyBody } from "./timeline/ReplyBody.tsx"
|
||||||
import { useMediaContent } from "./timeline/content/useMediaContent.tsx"
|
import { useMediaContent } from "./timeline/content/useMediaContent.tsx"
|
||||||
|
@ -27,63 +27,78 @@ import "./MessageComposer.css"
|
||||||
|
|
||||||
interface MessageComposerProps {
|
interface MessageComposerProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
setTextRows: (rows: number) => void
|
scrollToBottomRef: React.RefObject<() => void>
|
||||||
replyTo: MemDBEvent | null
|
setReplyToRef: React.RefObject<(evt: EventID | null) => void>
|
||||||
closeReply: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ComposerState {
|
||||||
|
text: string
|
||||||
|
media: MediaMessageEventContent | null
|
||||||
|
replyTo: EventID | null
|
||||||
|
uninited?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyComposer: ComposerState = { text: "", media: null, replyTo: null }
|
||||||
|
const uninitedComposer: ComposerState = { ...emptyComposer, uninited: true }
|
||||||
|
const composerReducer = (state: ComposerState, action: Partial<ComposerState>) =>
|
||||||
|
({ ...state, ...action, uninited: undefined })
|
||||||
|
|
||||||
const draftStore = {
|
const draftStore = {
|
||||||
get: (roomID: RoomID) => localStorage.getItem(`draft-${roomID}`) ?? "",
|
get: (roomID: RoomID): ComposerState | null => {
|
||||||
set: (roomID: RoomID, text: string) => localStorage.setItem(`draft-${roomID}`, text),
|
const data = localStorage.getItem(`draft-${roomID}`)
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(data)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (roomID: RoomID, data: ComposerState) => localStorage.setItem(`draft-${roomID}`, JSON.stringify(data)),
|
||||||
clear: (roomID: RoomID)=> localStorage.removeItem(`draft-${roomID}`),
|
clear: (roomID: RoomID)=> localStorage.removeItem(`draft-${roomID}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComposerProps) => {
|
const MessageComposer = ({ room, scrollToBottomRef, setReplyToRef }: MessageComposerProps) => {
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const [text, setText] = useState("")
|
const [state, setState] = useReducer(composerReducer, uninitedComposer)
|
||||||
const [media, setMedia] = useState(null)
|
|
||||||
const [loadingMedia, setLoadingMedia] = useState(false)
|
const [loadingMedia, setLoadingMedia] = useState(false)
|
||||||
const fileInput = useRef<HTMLInputElement>(null)
|
const fileInput = useRef<HTMLInputElement>(null)
|
||||||
|
const textInput = useRef<HTMLTextAreaElement>(null)
|
||||||
const textRows = useRef(1)
|
const textRows = useRef(1)
|
||||||
const typingSentAt = useRef(0)
|
const typingSentAt = useRef(0)
|
||||||
const fullSetText = useCallback((text: string, setDraft: boolean) => {
|
const replyToEvt = useRoomEvent(room, state.replyTo)
|
||||||
setText(text)
|
setReplyToRef.current = useCallback((evt: EventID | null) => {
|
||||||
textRows.current = text === "" ? 1 : text.split("\n").length
|
setState({ replyTo: evt })
|
||||||
setTextRows(textRows.current)
|
}, [])
|
||||||
if (setDraft) {
|
|
||||||
if (text === "") {
|
|
||||||
draftStore.clear(room.roomID)
|
|
||||||
} else {
|
|
||||||
draftStore.set(room.roomID, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [setTextRows, room.roomID])
|
|
||||||
const sendMessage = useCallback((evt: React.FormEvent) => {
|
const sendMessage = useCallback((evt: React.FormEvent) => {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
if (text === "" && !media) {
|
if (state.text === "" && !state.media) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullSetText("", true)
|
setState(emptyComposer)
|
||||||
setMedia(null)
|
|
||||||
closeReply()
|
|
||||||
const room_id = room.roomID
|
|
||||||
const mentions: Mentions = {
|
const mentions: Mentions = {
|
||||||
user_ids: [],
|
user_ids: [],
|
||||||
room: false,
|
room: false,
|
||||||
}
|
}
|
||||||
if (replyTo) {
|
if (replyToEvt) {
|
||||||
mentions.user_ids.push(replyTo.sender)
|
mentions.user_ids.push(replyToEvt.sender)
|
||||||
}
|
}
|
||||||
client.sendMessage({ room_id, base_content: media ?? undefined, text, reply_to: replyTo?.event_id, mentions })
|
client.sendMessage({
|
||||||
.catch(err => window.alert("Failed to send message: " + err))
|
room_id: room.roomID,
|
||||||
}, [fullSetText, closeReply, replyTo, media, text, room, client])
|
base_content: state.media ?? undefined,
|
||||||
|
text: state.text,
|
||||||
|
reply_to: replyToEvt?.event_id,
|
||||||
|
mentions,
|
||||||
|
}).catch(err => window.alert("Failed to send message: " + err))
|
||||||
|
}, [replyToEvt, state, room, client])
|
||||||
const onKeyDown = useCallback((evt: React.KeyboardEvent) => {
|
const onKeyDown = useCallback((evt: React.KeyboardEvent) => {
|
||||||
if (evt.key === "Enter" && !evt.shiftKey) {
|
if (evt.key === "Enter" && !evt.shiftKey) {
|
||||||
sendMessage(evt)
|
sendMessage(evt)
|
||||||
}
|
}
|
||||||
}, [sendMessage])
|
}, [sendMessage])
|
||||||
const onChange = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = useCallback((evt: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
fullSetText(evt.target.value, true)
|
setState({ text: evt.target.value })
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) {
|
if (evt.target.value !== "" && typingSentAt.current + 5_000 < now) {
|
||||||
typingSentAt.current = now
|
typingSentAt.current = now
|
||||||
|
@ -94,13 +109,7 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
|
||||||
client.rpc.setTyping(room.roomID, 0)
|
client.rpc.setTyping(room.roomID, 0)
|
||||||
.catch(err => console.error("Failed to send stop typing notification:", err))
|
.catch(err => console.error("Failed to send stop typing notification:", err))
|
||||||
}
|
}
|
||||||
}, [client, room.roomID, fullSetText])
|
}, [client, room.roomID])
|
||||||
const openFilePicker = useCallback(() => {
|
|
||||||
fileInput.current!.click()
|
|
||||||
}, [])
|
|
||||||
const clearMedia = useCallback(() => {
|
|
||||||
setMedia(null)
|
|
||||||
}, [])
|
|
||||||
const onAttachFile = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
const onAttachFile = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setLoadingMedia(true)
|
setLoadingMedia(true)
|
||||||
const file = evt.target.files![0]
|
const file = evt.target.files![0]
|
||||||
|
@ -114,7 +123,7 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(json.error)
|
throw new Error(json.error)
|
||||||
} else {
|
} else {
|
||||||
setMedia(json)
|
setState({ media: json })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => window.alert("Failed to upload file: " + err))
|
.catch(err => window.alert("Failed to upload file: " + err))
|
||||||
|
@ -123,7 +132,8 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
|
||||||
// To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState
|
// To ensure the cursor jumps to the end, do this in an effect rather than as the initial value of useState
|
||||||
// To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect
|
// To try to avoid the input bar flashing, use useLayoutEffect instead of useEffect
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
fullSetText(draftStore.get(room.roomID), false)
|
const draft = draftStore.get(room.roomID)
|
||||||
|
setState(draft ?? emptyComposer)
|
||||||
return () => {
|
return () => {
|
||||||
if (typingSentAt.current > 0) {
|
if (typingSentAt.current > 0) {
|
||||||
typingSentAt.current = 0
|
typingSentAt.current = 0
|
||||||
|
@ -131,16 +141,43 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
|
||||||
.catch(err => console.error("Failed to send stop typing notification due to room switch:", err))
|
.catch(err => console.error("Failed to send stop typing notification due to room switch:", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [client, room.roomID, fullSetText])
|
}, [client, room.roomID])
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!textInput.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// This is a hacky way to auto-resize the text area. Setting the rows to 1 and then
|
||||||
|
// checking scrollHeight seems to be the only reliable way to get the size of the text.
|
||||||
|
textInput.current.rows = 1
|
||||||
|
const newTextRows = (textInput.current.scrollHeight - 16) / 20
|
||||||
|
textInput.current.rows = newTextRows
|
||||||
|
textRows.current = newTextRows
|
||||||
|
// This has to be called unconditionally, because setting rows = 1 messes up the scroll state otherwise
|
||||||
|
scrollToBottomRef.current?.()
|
||||||
|
}, [state, scrollToBottomRef])
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.uninited) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!state.text && !state.media && !state.replyTo) {
|
||||||
|
draftStore.clear(room.roomID)
|
||||||
|
} else {
|
||||||
|
draftStore.set(room.roomID, state)
|
||||||
|
}
|
||||||
|
}, [room, state])
|
||||||
|
const openFilePicker = useCallback(() => fileInput.current!.click(), [])
|
||||||
|
const clearMedia = useCallback(() => setState({ media: null }), [])
|
||||||
|
const closeReply = useCallback(() => setState({ replyTo: null }), [])
|
||||||
return <div className="message-composer">
|
return <div className="message-composer">
|
||||||
{replyTo && <ReplyBody room={room} event={replyTo} onClose={closeReply}/>}
|
{replyToEvt && <ReplyBody room={room} event={replyToEvt} onClose={closeReply}/>}
|
||||||
{loadingMedia && <div className="composer-media"><ScaleLoader/></div>}
|
{loadingMedia && <div className="composer-media"><ScaleLoader/></div>}
|
||||||
{media && <ComposerMedia content={media} clearMedia={clearMedia}/>}
|
{state.media && <ComposerMedia content={state.media} clearMedia={clearMedia}/>}
|
||||||
<div className="input-area">
|
<div className="input-area">
|
||||||
<textarea
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
|
ref={textInput}
|
||||||
rows={textRows.current}
|
rows={textRows.current}
|
||||||
value={text}
|
value={state.text}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder="Send a message"
|
placeholder="Send a message"
|
||||||
|
@ -148,10 +185,13 @@ const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComp
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={openFilePicker}
|
onClick={openFilePicker}
|
||||||
disabled={!!media || loadingMedia}
|
disabled={!!state.media || loadingMedia}
|
||||||
title={media ? "You can only attach one file at a time" : ""}
|
title={state.media ? "You can only attach one file at a time" : ""}
|
||||||
><AttachIcon/></button>
|
><AttachIcon/></button>
|
||||||
<button onClick={sendMessage} disabled={(!text && !media) || loadingMedia}><SendIcon/></button>
|
<button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={(!state.text && !state.media) || loadingMedia}
|
||||||
|
><SendIcon/></button>
|
||||||
<input ref={fileInput} onChange={onAttachFile} type="file" value="" style={{ display: "none" }}/>
|
<input ref={fileInput} onChange={onAttachFile} type="file" value="" style={{ display: "none" }}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use, useCallback, useState } from "react"
|
import { use, useRef } from "react"
|
||||||
import { getMediaURL } from "@/api/media.ts"
|
import { getMediaURL } from "@/api/media.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { EventID } from "@/api/types"
|
||||||
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
import { useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||||
import { LightboxContext } from "./Lightbox.tsx"
|
import { LightboxContext } from "./Lightbox.tsx"
|
||||||
import MessageComposer from "./MessageComposer.tsx"
|
import MessageComposer from "./MessageComposer.tsx"
|
||||||
|
@ -53,13 +53,12 @@ const onKeyDownRoomView = (evt: React.KeyboardEvent) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomView = ({ room, clearActiveRoom }: RoomViewProps) => {
|
const RoomView = ({ room, clearActiveRoom }: RoomViewProps) => {
|
||||||
const [replyTo, setReplyTo] = useState<MemDBEvent | null>(null)
|
const scrollToBottomRef = useRef<() => void>(() => {})
|
||||||
const [textRows, setTextRows] = useState(1)
|
const setReplyToRef = useRef<(evt: EventID | null) => void>(() => {})
|
||||||
const closeReply = useCallback(() => setReplyTo(null), [])
|
|
||||||
return <div className="room-view" onKeyDown={onKeyDownRoomView} tabIndex={-1}>
|
return <div className="room-view" onKeyDown={onKeyDownRoomView} tabIndex={-1}>
|
||||||
<RoomHeader room={room} clearActiveRoom={clearActiveRoom}/>
|
<RoomHeader room={room} clearActiveRoom={clearActiveRoom}/>
|
||||||
<TimelineView room={room} textRows={textRows} replyTo={replyTo} setReplyTo={setReplyTo}/>
|
<TimelineView room={room} scrollToBottomRef={scrollToBottomRef} setReplyToRef={setReplyToRef}/>
|
||||||
<MessageComposer room={room} setTextRows={setTextRows} replyTo={replyTo} closeReply={closeReply}/>
|
<MessageComposer room={room} scrollToBottomRef={scrollToBottomRef} setReplyToRef={setReplyToRef}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import React, { use, useCallback } from "react"
|
import React, { use, useCallback } from "react"
|
||||||
import { getAvatarURL } from "@/api/media.ts"
|
import { getAvatarURL } from "@/api/media.ts"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
import { EventID, MemDBEvent, MemberEventContent, UnreadType } from "@/api/types"
|
||||||
import { isEventID } from "@/util/validation.ts"
|
import { isEventID } from "@/util/validation.ts"
|
||||||
import { ClientContext } from "../ClientContext.ts"
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
import { LightboxContext } from "../Lightbox.tsx"
|
import { LightboxContext } from "../Lightbox.tsx"
|
||||||
|
@ -36,7 +36,7 @@ export interface TimelineEventProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
evt: MemDBEvent
|
evt: MemDBEvent
|
||||||
prevEvt: MemDBEvent | null
|
prevEvt: MemDBEvent | null
|
||||||
setReplyTo: (evt: MemDBEvent) => void
|
setReplyToRef: React.RefObject<(evt: EventID | null) => void>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> {
|
function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> {
|
||||||
|
@ -109,8 +109,8 @@ function isSmallEvent(bodyType: React.FunctionComponent<EventContentProps>): boo
|
||||||
return bodyType === HiddenEvent || bodyType === MemberBody
|
return bodyType === HiddenEvent || bodyType === MemberBody
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) => {
|
const TimelineEvent = ({ room, evt, prevEvt, setReplyToRef }: TimelineEventProps) => {
|
||||||
const wrappedSetReplyTo = useCallback(() => setReplyTo(evt), [evt, setReplyTo])
|
const wrappedSetReplyTo = useCallback(() => setReplyToRef.current(evt.event_id), [evt, setReplyToRef])
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const memberEvt = room.getStateEvent("m.room.member", evt.sender)
|
const memberEvt = room.getStateEvent("m.room.member", evt.sender)
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
|
|
|
@ -13,9 +13,9 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { use, useCallback, useEffect, useLayoutEffect, useRef } from "react"
|
import React, { use, useCallback, useEffect, useLayoutEffect, useRef } from "react"
|
||||||
import { RoomStateStore, useRoomTimeline } from "@/api/statestore"
|
import { RoomStateStore, useRoomTimeline } from "@/api/statestore"
|
||||||
import { MemDBEvent } from "@/api/types"
|
import { EventID, MemDBEvent } from "@/api/types"
|
||||||
import useFocus from "@/util/focus.ts"
|
import useFocus from "@/util/focus.ts"
|
||||||
import { ClientContext } from "../ClientContext.ts"
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
import TimelineEvent from "./TimelineEvent.tsx"
|
import TimelineEvent from "./TimelineEvent.tsx"
|
||||||
|
@ -23,12 +23,11 @@ import "./TimelineView.css"
|
||||||
|
|
||||||
interface TimelineViewProps {
|
interface TimelineViewProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
textRows: number
|
scrollToBottomRef: React.RefObject<() => void>
|
||||||
replyTo: MemDBEvent | null
|
setReplyToRef: React.RefObject<(evt: EventID | null) => void>
|
||||||
setReplyTo: (evt: MemDBEvent) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps) => {
|
const TimelineView = ({ room, scrollToBottomRef, setReplyToRef }: TimelineViewProps) => {
|
||||||
const timeline = useRoomTimeline(room)
|
const timeline = useRoomTimeline(room)
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const loadHistory = useCallback(() => {
|
const loadHistory = useCallback(() => {
|
||||||
|
@ -56,6 +55,8 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
||||||
if (timelineViewRef.current) {
|
if (timelineViewRef.current) {
|
||||||
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
||||||
}
|
}
|
||||||
|
scrollToBottomRef.current = () =>
|
||||||
|
bottomRef.current && scrolledToBottom.current && bottomRef.current.scrollIntoView()
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (bottomRef.current && scrolledToBottom.current) {
|
if (bottomRef.current && scrolledToBottom.current) {
|
||||||
// For any timeline changes, if we were at the bottom, scroll to the new bottom
|
// For any timeline changes, if we were at the bottom, scroll to the new bottom
|
||||||
|
@ -65,7 +66,7 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
||||||
timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current
|
timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current
|
||||||
}
|
}
|
||||||
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
||||||
}, [textRows, replyTo, timeline])
|
}, [timeline])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newestEvent = timeline[timeline.length - 1]
|
const newestEvent = timeline[timeline.length - 1]
|
||||||
if (
|
if (
|
||||||
|
@ -114,7 +115,7 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const thisEvt = <TimelineEvent
|
const thisEvt = <TimelineEvent
|
||||||
key={entry.rowid} room={room} evt={entry} prevEvt={prevEvt} setReplyTo={setReplyTo}
|
key={entry.rowid} room={room} evt={entry} prevEvt={prevEvt} setReplyToRef={setReplyToRef}
|
||||||
/>
|
/>
|
||||||
prevEvt = entry
|
prevEvt = entry
|
||||||
return thisEvt
|
return thisEvt
|
||||||
|
|
Loading…
Add table
Reference in a new issue