Adding scroll to the end when you are in the end or is the first render.

This commit is contained in:
Sergiotarxz 2023-06-19 17:15:00 +02:00
parent 370e6f536c
commit 9c5c967e65
13 changed files with 256 additions and 101 deletions

View File

@ -40,45 +40,52 @@ export default function Game (props: GameProps): JSX.Element {
const [connectedLocations, setConnectedLocations] = React.useState<Location[] | null>(null)
const [logLines, setLogLines] = React.useState<LogLine[] | null>(null)
const [error, setError] = React.useState<string | null>(null)
if (websocket === null) {
window.setTimeout(() => {
const locationProtocol = window.location.protocol
if (locationProtocol == null) {
return
const [scrollLog, setScrollLog] = React.useState<number | null>(null)
const logPresentationRef = React.useRef<HTMLDivElement>(null)
window.setTimeout(() => {
setWebsocket((websocket): WebSocket | null => {
if (websocket === null) {
const locationProtocol = window.location.protocol
if (locationProtocol == null) {
return null
}
const protocol = locationProtocol.match(/https:/) != null ? 'wss' : 'ws'
const webSocket = new WebSocket(`${protocol}://${window.location.host}/ws`)
webSocket.onopen = () => {
new OutputPacketInit(selectedPJ.uuid).send(webSocket)
let interval: number = 0
interval = window.setInterval(() => {
if (webSocket.readyState === WebSocket.OPEN) {
new OutputPacketPing().send(webSocket)
return
}
window.clearInterval(interval)
}, 250000)
}
const inputPackets = new InputPackets(setTeamPJs,
setCurrentLocation, setConnectedLocations,
logLines, setLogLines, setError, setScrollLog,
logPresentationRef)
webSocket.onmessage = (event) => {
const packet = JSON.parse(event.data)
inputPackets.handle(packet)
}
webSocket.onerror = (event) => {
console.log(event)
}
return webSocket
}
const protocol = locationProtocol.match(/https:/) != null ? 'wss' : 'ws'
const webSocket = new WebSocket(`${protocol}://${window.location.host}/ws`)
webSocket.onopen = () => {
new OutputPacketInit(selectedPJ.uuid).send(webSocket)
let interval: number = 0
interval = window.setInterval(() => {
if (webSocket.readyState === WebSocket.OPEN) {
new OutputPacketPing().send(webSocket)
return
}
window.clearInterval(interval)
}, 10000)
}
const inputPackets = new InputPackets(setTeamPJs,
setCurrentLocation, setConnectedLocations,
logLines, setLogLines, setError)
webSocket.onmessage = (event) => {
const packet = JSON.parse(event.data)
inputPackets.handle(packet)
}
webSocket.onerror = (event) => {
console.log(event)
}
setWebsocket(webSocket)
}, 1)
}
return websocket
})
}, 100)
return (
<>
<UpperPanel teamPJs={teamPJs}
currentLocation={currentLocation}
connectedLocations={connectedLocations}
logLines={logLines}
websocket={websocket}/>
websocket={websocket}
logPresentationRef={logPresentationRef}/>
<BottomPanel/>
</>
)

View File

@ -0,0 +1,50 @@
import * as React from 'react'
import type { LogLine } from '@lastres/log-line'
export interface LogPanelProps {
logLines: LogLine[] | null
}
interface Style {
color?: string
background?: string
}
export default function LogPanel (props: LogPanelProps): JSX.Element {
const logLines = props.logLines
function generateLog (): React.ReactNode {
if (logLines === null || logLines.length < 1) {
return (
<>No log</>
)
}
return logLines.sort(function (a: LogLine, b: LogLine): number {
const aDate = Date.parse(a.date)
const bDate = Date.parse(b.date)
return aDate - bDate
}).map((item, i) => {
return <span key={i}>
<b>{item.date}</b> {
item.content.map((item, i) => {
const style: Style = {}
if (item.color !== undefined) {
style.color = item.color
}
if (item.background !== undefined) {
style.background = item.background
}
return <span key={i} style={style}>{item.text}</span>
})
} <br/>
</span>
})
}
return (
<code><pre>
{
generateLog()
}
</pre></code>
)
}

View File

@ -0,0 +1,20 @@
import * as React from 'react'
export interface PresentationItemProps {
children: React.ReactNode
presentationItemRef?: React.RefObject<HTMLDivElement>
}
export default function PresentationItem (props: PresentationItemProps): JSX.Element {
if (props.presentationItemRef === undefined) {
return (
<div className="presentation-item">
{props.children}
</div>
)
}
return (
<div ref={props.presentationItemRef} className="presentation-item">
{props.children}
</div>
)
}

View File

@ -0,0 +1,12 @@
import * as React from 'react'
export interface PresentationProps {
children: React.ReactNode
}
export default function Presentation (props: PresentationProps): JSX.Element {
return (
<div className="presentation">
{props.children}
</div>
)
}

View File

@ -3,6 +3,9 @@ import type { Location } from '@lastres/location'
import type { PJ } from '@lastres/pj'
import type { LogLine } from '@lastres/log-line'
import PJListItem from '@lastres/components/pj-list-item'
import PresentationItem from '@lastres/components/presentation-item'
import Presentation from '@lastres/components/presentation'
import LogPanel from '@lastres/components/log-panel'
import MoveToPacket from '@lastres/output-packet/move-to'
interface UpperPanelProps {
@ -11,11 +14,7 @@ interface UpperPanelProps {
currentLocation: Location | null
logLines: LogLine[] | null
websocket: WebSocket | null
}
interface Style {
color?: string
background?: string
logPresentationRef: React.RefObject<HTMLDivElement>
}
export default function UpperPanel (props: UpperPanelProps): JSX.Element {
@ -24,6 +23,7 @@ export default function UpperPanel (props: UpperPanelProps): JSX.Element {
const currentLocation = props.currentLocation
const logLines = props.logLines
const websocket = props.websocket
const logPresentationRef = props.logPresentationRef
if (!(teamPJs !== null && currentLocation !== null && connectedLocations !== null)) {
return (
<>
@ -39,50 +39,19 @@ export default function UpperPanel (props: UpperPanelProps): JSX.Element {
}
}
function generateLog (): React.ReactNode {
if (logLines === null || logLines.length < 1) {
return (
<>No log</>
)
}
return logLines.sort(function (a: LogLine, b: LogLine): number {
const aDate = Date.parse(a.date)
const bDate = Date.parse(b.date)
return aDate - bDate
}).map((item, i) => {
return <span key={i}>
<b>{item.date}</b> {
item.content.map((item, i) => {
const style: Style = {}
if (item.color !== undefined) {
style.color = item.color
}
if (item.background !== undefined) {
style.background = item.background
}
return <span key={i} style={style}>{item.text}</span>
})
} <br/>
</span>
})
}
return (
<div className="presentation">
<div className="presentation-item">
<Presentation>
<PresentationItem>
{
teamPJs.map((item, i) => {
return <PJListItem key={i} pj={item}/>
})
}
</div>
<div className="presentation-item">
<code><pre>
{
generateLog()
}
</pre></code>
</div>
<div className="presentation-item">
</PresentationItem>
<PresentationItem presentationItemRef={logPresentationRef}>
<LogPanel logLines={logLines}/>
</PresentationItem>
<PresentationItem>
<p>Estás en {currentLocation.area.name}/{currentLocation.location.name}.</p>
<p>Puedes ir a:</p>
<ul>
@ -96,7 +65,7 @@ export default function UpperPanel (props: UpperPanelProps): JSX.Element {
})
}
</ul>
</div>
</div>
</PresentationItem>
</Presentation>
)
}

View File

@ -12,6 +12,9 @@ type DispatchHash = Record<string, any>
type SetLogLinesCallback = (set: LogLine[] | null) => LogLine[]
type SetLogLines = (set: LogLine[] | SetLogLinesCallback | null) => void
type SetError = (set: string | null) => void
type SetScrollLogCallback = (set: number | null) => number | null
type SetScrollLog = (set: number | null | SetScrollLogCallback) => void
type LogPresentationRef = React.RefObject<HTMLDivElement>
interface Packet {
command: string
@ -19,6 +22,7 @@ interface Packet {
}
type LogHash = Record<string, LogLine>
export default class InputPackets {
setTeamPJs: SetTeamPJs
setCurrentLocation: SetCurrentLocation
@ -28,18 +32,24 @@ export default class InputPackets {
cachedHash: DispatchHash | null = null
cachedArray: InputPacket[] | null = null
setError: SetError
setScrollLog: SetScrollLog
logPresentationRef: LogPresentationRef
constructor (setTeamPJs: SetTeamPJs,
setCurrentLocation: SetCurrentLocation,
setConnectedLocations: SetConnectedLocations,
logLines: LogLine[] | null,
setLogLines: SetLogLines,
setError: SetError) {
setError: SetError,
setScrollLog: SetScrollLog,
logPresentationRef: LogPresentationRef) {
this.setTeamPJs = setTeamPJs
this.setCurrentLocation = setCurrentLocation
this.setConnectedLocations = setConnectedLocations
this.logLines = logLines
this.setLogLines = setLogLines
this.setError = setError
this.setScrollLog = setScrollLog
this.logPresentationRef = logPresentationRef
}
handle (packet: Packet): void {
@ -50,10 +60,40 @@ export default class InputPackets {
}
listAvailablePackets (): InputPacket[] {
let firstTime = true
if (this.cachedArray === null) {
const infoPacket = new InputPacketInfo()
const pongPacket = new InputPacketPong()
infoPacket.onReceive((data) => {
const logPresentationRef = this.logPresentationRef
let scrollData: number[] = []
function saveScroll (): void {
if (logPresentationRef.current === null) {
return
}
scrollData = [logPresentationRef.current.scrollHeight, logPresentationRef.current.scrollTop, logPresentationRef.current.offsetHeight]
}
function applyScroll (): void {
if (scrollData.length < 3) {
return
}
if (logPresentationRef.current === null) {
return
}
const logPresentation = logPresentationRef.current
const [scrollHeight, scrollTop, offsetHeight] = scrollData
if (firstTime) {
firstTime = false
return
}
if (scrollHeight === offsetHeight) {
logPresentation.scrollTo(0, logPresentation.scrollHeight)
return
}
if (scrollHeight <= scrollTop + offsetHeight) {
logPresentation.scrollTo(0, logPresentation.scrollHeight)
}
}
if (data.error !== undefined) {
this.setError(data.error)
return
@ -68,10 +108,15 @@ export default class InputPackets {
this.setCurrentLocation(data.location_data.current)
}
if (data.set_log !== undefined) {
saveScroll()
this.setLogLines(data.set_log)
window.setTimeout(() => {
applyScroll()
}, 10)
}
if (data.append_log !== undefined) {
const logHash: LogHash = {}
saveScroll()
this.setLogLines((logLines: LogLine[] | null): LogLine[] => {
if (logLines !== null) {
for (const item of logLines) {
@ -85,6 +130,9 @@ export default class InputPackets {
}
return []
})
window.setTimeout(() => {
applyScroll()
}, 10)
}
})
this.cachedArray = [infoPacket, pongPacket]

View File

@ -7,3 +7,6 @@ database:
port: 5432
user: lastres
password: topsecret
hypnotoad:
listen:
- http://*:3000

View File

@ -22,6 +22,7 @@ my $redis = LasTres::Redis->new;
my $input_packets = LasTres::Controller::Websocket::InputPackets->new;
sub ws ($self) {
$self->inactivity_timeout(300);
my $user = $self->user;
if ( !defined $user ) {
return $self->render(

View File

@ -27,8 +27,7 @@ sub identifier {
sub handle ( $self, $ws, $session, $data ) {
if ( ref $data ne 'HASH' ) {
return $ws->send(
to_json( { error => "Data should be a hashref." } ) );
return $ws->send( to_json( { error => "Data should be a hashref." } ) );
}
my $pj_uuid = $data->{pj_uuid};
if ( !defined $pj_uuid ) {
@ -36,8 +35,7 @@ sub handle ( $self, $ws, $session, $data ) {
}
my @pjs = $result_set_pjs->search( { uuid => $pj_uuid } );
if ( !scalar @pjs ) {
return $ws->send(
to_json( { error => 'This pj does not exists' } ) );
return $ws->send( to_json( { error => 'This pj does not exists' } ) );
}
my $pj = $pjs[0];
my $user = $session->{user};
@ -54,10 +52,7 @@ sub handle ( $self, $ws, $session, $data ) {
my $connected_places = $self->_get_connected_places($pj);
$pj->append_log_line(
[
{ text => 'Nueva conexion a este pj.', color => 'red' },
]
);
[ { text => 'Nueva conexion a este pj.', color => 'red', background => 'black' }, ] );
if ( !$pj->get_flag( LasTres::Flags::INTRO_MESSAGE_SENT_FLAG() ) ) {
$pj->set_flag(LasTres::Flags::INTRO_MESSAGE_SENT_FLAG);
@ -73,7 +68,7 @@ sub handle ( $self, $ws, $session, $data ) {
my $info_packet_to_send =
LasTres::Controller::Websocket::OutputPacket::Info->new(
set_log => [$pj->last_50_log],
set_log => [ $pj->last_50_log ],
team_pjs => $team_pjs,
location_data => {
current => $location->hash,
@ -83,23 +78,41 @@ sub handle ( $self, $ws, $session, $data ) {
);
$info_packet_to_send->send($ws);
my $redis = LasTres::Redis->new;
$redis->subscribe($redis->pj_subscription($pj), my $save = sub($message, $topic, $topics) {
return $self->_on_redis_event($ws, $session, $message, $topic, $topics);
});
$redis->subscribe(
$redis->pj_subscription($pj),
my $save = sub ( $message, $topic, $topics ) {
return $self->_on_redis_event( $ws, $session, $message, $topic,
$topics );
}
);
$session->{redis} = $redis;
}
sub _on_redis_event($self, $ws, $session, $message, $topic, $topics) {
say 'Receiving Redis event ' . $topic;
sub _on_redis_event ( $self, $ws, $session, $message, $topic, $topics ) {
my $data = from_json($message);
my $pj = $session->{pj};
if ($data->{command} eq 'append-log') {
$session->{pj} = $session->{pj}->get_from_storage;
my $pj = $session->{pj};
if ( $data->{command} eq 'append-log' ) {
my $info_packet_to_send =
LasTres::Controller::Websocket::OutputPacket::Info->new(
append_log => $data->{log}
);
append_log => $data->{log} );
$info_packet_to_send->send($ws);
return;
}
if ( $data->{command} eq 'update-location' ) {
my $team = $pj->team;
my $location = $team->location;
my $connected_places = $self->_get_connected_places($pj);
my $info_packet_to_send =
LasTres::Controller::Websocket::OutputPacket::Info->new(
location_data => {
current => $location->hash,
connected_places => $connected_places,
},
);
$info_packet_to_send->send($ws);
return;
}
}

View File

@ -29,8 +29,11 @@ sub show_intro ( $self, $pj ) {
}
sub notify_arrival($self, $team) {
require LasTres::Redis;
my $redis = LasTres::Redis->new;
for my $pj ($team->members) {
$self->show_intro($pj);
$redis->publish( $redis->pj_subscription($pj), to_json({ command => 'update-location' }));
}
}

View File

@ -290,7 +290,6 @@ sub append_log_line ( $self, $content ) {
$log->insert;
$log = $log->get_from_storage;
say 'Sending redis event '. $redis->pj_subscription($self);
$redis->publish( $redis->pj_subscription($self),
to_json( { command => 'append-log', log => $log->hash } ) );
}

BIN
public/img/bulldog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

File diff suppressed because one or more lines are too long