Adding scroll to the end when you are in the end or is the first render.
This commit is contained in:
parent
370e6f536c
commit
9c5c967e65
@ -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/>
|
||||
</>
|
||||
)
|
||||
|
50
js-src/components/log-panel.tsx
Normal file
50
js-src/components/log-panel.tsx
Normal 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>
|
||||
)
|
||||
}
|
20
js-src/components/presentation-item.tsx
Normal file
20
js-src/components/presentation-item.tsx
Normal 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>
|
||||
)
|
||||
}
|
12
js-src/components/presentation.tsx
Normal file
12
js-src/components/presentation.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -7,3 +7,6 @@ database:
|
||||
port: 5432
|
||||
user: lastres
|
||||
password: topsecret
|
||||
hypnotoad:
|
||||
listen:
|
||||
- http://*:3000
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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' }));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
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
Loading…
x
Reference in New Issue
Block a user