mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-16 21:23:20 +00:00
1086 lines
32 KiB
TypeScript
1086 lines
32 KiB
TypeScript
import { filter } from 'common/collections';
|
|
import { BooleanLike } from 'common/react';
|
|
import { decodeHtmlEntities, toTitleCase } from 'common/string';
|
|
import { Fragment } from 'inferno';
|
|
import { useBackend, useLocalState } from '../backend';
|
|
import { Box, ByondUi, Button, Flex, Icon, LabeledList, Input, Section, Table } from '../components';
|
|
import { Window } from '../layouts';
|
|
import { CrewManifestContent } from './CrewManifest';
|
|
|
|
const HOMETAB = 1;
|
|
const PHONTAB = 2;
|
|
const CONTTAB = 3;
|
|
const MESSTAB = 4;
|
|
const MESSSUBTAB = 40;
|
|
const NEWSTAB = 5;
|
|
const NOTETAB = 6;
|
|
const WTHRTAB = 7;
|
|
const MANITAB = 8;
|
|
const SETTTAB = 9;
|
|
|
|
let TabToTemplate = {}; // Populated under each template
|
|
|
|
type Data = {
|
|
// GENERAL
|
|
currentTab: number;
|
|
video_comm: BooleanLike;
|
|
mapRef: string;
|
|
|
|
// FOOTER
|
|
time: string;
|
|
connectionStatus: BooleanLike;
|
|
owner: string;
|
|
occupation: string;
|
|
|
|
// HEADER
|
|
flashlight: BooleanLike;
|
|
|
|
// NOTIFICATIONS
|
|
voice_mobs: [];
|
|
communicating: [];
|
|
requestsReceived: [];
|
|
invitesSent: [];
|
|
};
|
|
|
|
export const Communicator = (props, context) => {
|
|
const { act, data } = useBackend<Data>(context);
|
|
|
|
const { currentTab, video_comm, mapRef } = data;
|
|
|
|
/* 0: Fullscreen Video
|
|
* 1: Popup Video
|
|
* 2: Minimized Video
|
|
*/
|
|
const [videoSetting, setVideoSetting] = useLocalState(context, 'videoSetting', 0);
|
|
|
|
return (
|
|
<Window width={475} height={700} resizable>
|
|
<Window.Content>
|
|
{video_comm && <VideoComm videoSetting={videoSetting} setVideoSetting={setVideoSetting} />}
|
|
{(!video_comm || videoSetting !== 0) && (
|
|
<Fragment>
|
|
<CommunicatorHeader />
|
|
<Box
|
|
height="88%"
|
|
mb={1}
|
|
style={{
|
|
'overflow-y': 'auto',
|
|
}}>
|
|
{TabToTemplate[currentTab] || <TemplateError />}
|
|
</Box>
|
|
<CommunicatorFooter videoSetting={videoSetting} setVideoSetting={setVideoSetting} />
|
|
</Fragment>
|
|
)}
|
|
</Window.Content>
|
|
</Window>
|
|
);
|
|
};
|
|
|
|
const VideoComm = (props, context) => {
|
|
const { act, data } = useBackend<Data>(context);
|
|
|
|
const { video_comm, mapRef } = data;
|
|
|
|
const { videoSetting, setVideoSetting } = props;
|
|
|
|
if (videoSetting === 0) {
|
|
return (
|
|
<Box width="100%" height="100%">
|
|
<ByondUi
|
|
width="100%"
|
|
height="95%"
|
|
params={{
|
|
id: mapRef,
|
|
type: 'map',
|
|
}}
|
|
/>
|
|
<Flex justify="space-between" spacing={1} mt={0.5}>
|
|
<Flex.Item grow={1}>
|
|
<Button textAlign="center" fluid fontSize={1.5} icon="window-minimize" onClick={() => setVideoSetting(1)} />
|
|
</Flex.Item>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
color="bad"
|
|
icon="video-slash"
|
|
onClick={() => act('endvideo')}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
color="bad"
|
|
icon="phone-slash"
|
|
onClick={() => act('hang_up')}
|
|
/>
|
|
</Flex.Item>
|
|
</Flex>
|
|
</Box>
|
|
);
|
|
} else if (videoSetting === 1) {
|
|
return (
|
|
<Box
|
|
style={{
|
|
'position': 'absolute',
|
|
'right': '5px',
|
|
'bottom': '50px',
|
|
'z-index': 1,
|
|
}}>
|
|
<Section p={0} m={0}>
|
|
<Flex justify="space-between" spacing={1}>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
icon="window-minimize"
|
|
onClick={() => setVideoSetting(2)}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
icon="window-maximize"
|
|
onClick={() => setVideoSetting(0)}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
color="bad"
|
|
icon="video-slash"
|
|
onClick={() => act('endvideo')}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item grow={1}>
|
|
<Button
|
|
textAlign="center"
|
|
fluid
|
|
fontSize={1.5}
|
|
color="bad"
|
|
icon="phone-slash"
|
|
onClick={() => act('hang_up')}
|
|
/>
|
|
</Flex.Item>
|
|
</Flex>
|
|
</Section>
|
|
<ByondUi
|
|
width="200px"
|
|
height="200px"
|
|
params={{
|
|
id: mapRef,
|
|
type: 'map',
|
|
}}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const TemplateError = (props, context) => {
|
|
const { act, data } = useBackend<Data>(context);
|
|
|
|
const { currentTab } = data;
|
|
|
|
return <Section title="Error!">You tried to access tab #{currentTab}, but there was no template defined!</Section>;
|
|
};
|
|
|
|
const CommunicatorHeader = (props, context) => {
|
|
const { act, data } = useBackend<Data>(context);
|
|
|
|
const { time, connectionStatus, owner, occupation } = data;
|
|
|
|
return (
|
|
<Section>
|
|
<Flex align="center" justify="space-between">
|
|
<Flex.Item color="average">{time}</Flex.Item>
|
|
<Flex.Item>
|
|
<Icon
|
|
color={connectionStatus === 1 ? 'good' : 'bad'}
|
|
name={connectionStatus === 1 ? 'signal' : 'exclamation-triangle'}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item color="average">{decodeHtmlEntities(owner)}</Flex.Item>
|
|
<Flex.Item color="average">{decodeHtmlEntities(occupation)}</Flex.Item>
|
|
</Flex>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
const CommunicatorFooter = (props, context) => {
|
|
const { act, data } = useBackend<Data>(context);
|
|
|
|
const { flashlight } = data;
|
|
|
|
const { videoSetting, setVideoSetting } = props;
|
|
|
|
return (
|
|
<Flex>
|
|
<Flex.Item basis={videoSetting === 2 ? '60%' : '80%'}>
|
|
<Button
|
|
p={1}
|
|
fluid
|
|
icon="home"
|
|
iconSize={2}
|
|
textAlign="center"
|
|
onClick={() => act('switch_tab', { switch_tab: HOMETAB })}
|
|
/>
|
|
</Flex.Item>
|
|
<Flex.Item basis="20%">
|
|
<Button
|
|
icon="lightbulb"
|
|
iconSize={2}
|
|
p={1}
|
|
fluid
|
|
textAlign="center"
|
|
selected={flashlight}
|
|
tooltip="Flashlight"
|
|
tooltipPosition="top"
|
|
onClick={() => act('Light')}
|
|
/>
|
|
</Flex.Item>
|
|
{videoSetting === 2 && (
|
|
<Flex.Item basis="20%">
|
|
<Button
|
|
icon="video"
|
|
iconSize={2}
|
|
p={1}
|
|
fluid
|
|
textAlign="center"
|
|
tooltip="Open Video"
|
|
tooltipPosition="top"
|
|
onClick={() => setVideoSetting(1)}
|
|
/>
|
|
</Flex.Item>
|
|
)}
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
/* Helper for notifications (yes this is a mess, but whatever, it works) */
|
|
const hasNotifications = (app, context) => {
|
|
const { data } = useBackend<Data>(context);
|
|
|
|
const {
|
|
/* Phone Notifications */
|
|
voice_mobs,
|
|
communicating,
|
|
requestsReceived,
|
|
invitesSent,
|
|
video_comm,
|
|
} = data;
|
|
|
|
if (app === 'Phone') {
|
|
if (voice_mobs.length || communicating.length || requestsReceived.length || invitesSent.length || video_comm) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/* Tabs Below this point */
|
|
|
|
type HomeTabData = {
|
|
homeScreen: { number: number; module: string; icon: string }[];
|
|
};
|
|
|
|
/* Home tab, provides access to all other tabs. */
|
|
const HomeTab = (props, context) => {
|
|
const { act, data } = useBackend<HomeTabData>(context);
|
|
|
|
const { homeScreen } = data;
|
|
|
|
return (
|
|
<Flex mt={2} wrap="wrap" align="center" justify="center">
|
|
{homeScreen.map((app) => (
|
|
<Flex.Item basis="25%" textAlign="center" mb={2} key={app.number}>
|
|
<Button
|
|
style={{
|
|
'border-radius': '10%',
|
|
'border': '1px solid #000',
|
|
}}
|
|
width="64px"
|
|
height="64px"
|
|
position="relative"
|
|
onClick={() => act('switch_tab', { switch_tab: app.number })}>
|
|
<Icon
|
|
spin={hasNotifications(app.module, context)}
|
|
color={hasNotifications(app.module, context) ? 'bad' : null}
|
|
name={app.icon}
|
|
position="absolute"
|
|
size={3}
|
|
top="25%"
|
|
left="25%"
|
|
/>
|
|
</Button>
|
|
<Box>{app.module}</Box>
|
|
</Flex.Item>
|
|
))}
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[HOMETAB] = <HomeTab />;
|
|
|
|
type PhoneTabData = {
|
|
targetAddress: string;
|
|
voice_mobs: { name: string; true_name: string; ref: string }[];
|
|
communicating: { address: string; name: string; true_name: string; ref: string }[];
|
|
requestsReceived: { address: string; name: string; ref: string }[];
|
|
invitesSent: { address: string; name: string }[];
|
|
video_comm: string;
|
|
selfie_mode: BooleanLike;
|
|
};
|
|
|
|
/* Phone tab, provides a phone interface! */
|
|
const PhoneTab = (props, context) => {
|
|
const { act, data } = useBackend<PhoneTabData>(context);
|
|
|
|
const { targetAddress, voice_mobs, communicating, requestsReceived, invitesSent, video_comm, selfie_mode } = data;
|
|
|
|
return (
|
|
<Section title="Phone">
|
|
<LabeledList>
|
|
<LabeledList.Item label="Target EPv2 Address" verticalAlign="middle">
|
|
<Flex align="center">
|
|
<Flex.Item grow={1}>
|
|
<Input fluid value={targetAddress} onInput={(e, val) => act('write_target_address', { val: val })} />
|
|
</Flex.Item>
|
|
<Flex.Item>
|
|
<Button icon="times" onClick={() => act('clear_target_address')} />
|
|
</Flex.Item>
|
|
</Flex>
|
|
</LabeledList.Item>
|
|
</LabeledList>
|
|
<NumberPad />
|
|
<Section title="Connection Management" mt={2}>
|
|
<LabeledList>
|
|
<LabeledList.Item label="Camera Mode">
|
|
<Button
|
|
fluid
|
|
content={selfie_mode ? 'Front-facing Camera' : 'Rear-facing Camera'}
|
|
onClick={() => act('selfie_mode')}
|
|
/>
|
|
</LabeledList.Item>
|
|
</LabeledList>
|
|
<Section title="External Connections">
|
|
{(!!voice_mobs.length && (
|
|
<LabeledList>
|
|
{voice_mobs.map((mob) => (
|
|
<LabeledList.Item label={decodeHtmlEntities(mob.name)} key={mob.ref}>
|
|
<Button
|
|
icon="times"
|
|
color="bad"
|
|
content="Disconnect"
|
|
onClick={() => act('disconnect', { disconnect: mob.true_name })}
|
|
/>
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
)) || <Box>No connections</Box>}
|
|
</Section>
|
|
<Section title="Internal Connections">
|
|
{(!!communicating.length && (
|
|
<Table>
|
|
{communicating.map((comm) => (
|
|
<Table.Row key={comm.address}>
|
|
<Table.Cell color="label">{decodeHtmlEntities(comm.name)}</Table.Cell>
|
|
<Table.Cell>
|
|
<Button
|
|
icon="times"
|
|
color="bad"
|
|
content="Disconnect"
|
|
onClick={() => act('disconnect', { disconnect: comm.true_name })}
|
|
/>
|
|
{(video_comm === null && (
|
|
<Button
|
|
icon="camera"
|
|
content="Start Video"
|
|
onClick={() => act('startvideo', { startvideo: comm.ref })}
|
|
/>
|
|
)) ||
|
|
(video_comm === comm.ref && (
|
|
<Button
|
|
icon="times"
|
|
color="bad"
|
|
content="Stop Video"
|
|
onClick={() => act('endvideo', { endvideo: comm.true_name })}
|
|
/>
|
|
))}
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
</Table>
|
|
)) || <Box>No connections</Box>}
|
|
</Section>
|
|
<Section title="Requests Received">
|
|
{(!!requestsReceived.length && (
|
|
<LabeledList>
|
|
{requestsReceived.map((request) => (
|
|
<LabeledList.Item label={decodeHtmlEntities(request.name)} key={request.address}>
|
|
<Box>{decodeHtmlEntities(request.address)}</Box>
|
|
<Box>
|
|
<Button icon="signal" content="Accept" onClick={() => act('dial', { dial: request.address })} />
|
|
<Button icon="times" content="Decline" onClick={() => act('decline', { decline: request.ref })} />
|
|
</Box>
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
)) || <Box>No requests received.</Box>}
|
|
</Section>
|
|
<Section title="Invites Sent">
|
|
{(!!invitesSent.length && (
|
|
<LabeledList>
|
|
{invitesSent.map((invite) => (
|
|
<LabeledList.Item label={decodeHtmlEntities(invite.name)} key={invite.address}>
|
|
<Box>{decodeHtmlEntities(invite.address)}</Box>
|
|
<Box>
|
|
<Button
|
|
icon="pen"
|
|
onClick={() => {
|
|
act('copy', { 'copy': invite.address });
|
|
}}
|
|
content="Copy"
|
|
/>
|
|
</Box>
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
)) || <Box>No invites sent.</Box>}
|
|
</Section>
|
|
</Section>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
// Subtemplate
|
|
const NumberPad = (props, context) => {
|
|
const { act, data } = useBackend<PhoneTabData>(context);
|
|
|
|
const { targetAddress } = data;
|
|
|
|
const validCharacters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
|
|
|
let buttonArray = validCharacters.map((char) => (
|
|
<Button key={char} content={char} fontSize={2} fluid onClick={() => act('add_hex', { add_hex: char })} />
|
|
));
|
|
|
|
let finalArray: any[] = [];
|
|
|
|
for (let i = 0; i < buttonArray.length; i += 4) {
|
|
finalArray.push(
|
|
<Table.Row>
|
|
<Table.Cell>{buttonArray[i]}</Table.Cell>
|
|
<Table.Cell>{buttonArray[i + 1]}</Table.Cell>
|
|
<Table.Cell>{buttonArray[i + 2]}</Table.Cell>
|
|
<Table.Cell>{buttonArray[i + 3]}</Table.Cell>
|
|
</Table.Row>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Flex align="center" justify="center" mt={1}>
|
|
<Flex.Item>
|
|
<Table>{finalArray}</Table>
|
|
<Flex width="100%" justify="space-between">
|
|
{/* Dial */}
|
|
<Flex.Item basis="33%">
|
|
<Button width="100%" height="64px" position="relative" onClick={() => act('dial', { dial: targetAddress })}>
|
|
<Icon name="phone" position="absolute" size={3} top="25%" left="25%" />
|
|
</Button>
|
|
<Box textAlign="center">Dial</Box>
|
|
</Flex.Item>
|
|
{/* Message */}
|
|
<Flex.Item basis="33%">
|
|
<Button
|
|
width="100%"
|
|
height="64px"
|
|
position="relative"
|
|
onClick={() => {
|
|
act('message', { message: targetAddress });
|
|
act('switch_tab', { switch_tab: MESSTAB });
|
|
}}>
|
|
<Icon name="comment-alt" position="absolute" size={3} top="25%" left="25%" />
|
|
</Button>
|
|
<Box textAlign="center">Message</Box>
|
|
</Flex.Item>
|
|
{/* Hang Up */}
|
|
<Flex.Item basis="33%">
|
|
<Button width="100%" height="64px" position="relative" onClick={() => act('hang_up')}>
|
|
<Icon name="times" position="absolute" size={3} top="25%" left="25%" />
|
|
</Button>
|
|
<Box textAlign="center">Hang Up</Box>
|
|
</Flex.Item>
|
|
</Flex>
|
|
</Flex.Item>
|
|
</Flex>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[PHONTAB] = <PhoneTab />;
|
|
|
|
type ContactsTabData = {
|
|
knownDevices: { address: string; name: string }[];
|
|
};
|
|
|
|
/* Contacts */
|
|
const ContactsTab = (props, context) => {
|
|
const { act, data } = useBackend<ContactsTabData>(context);
|
|
|
|
const { knownDevices } = data;
|
|
|
|
return (
|
|
<Section title="Known Devices">
|
|
{(knownDevices.length && (
|
|
<Table>
|
|
{knownDevices.map((device) => (
|
|
<Table.Row key={device.address}>
|
|
<Table.Cell
|
|
color="label"
|
|
style={{
|
|
'word-break': 'break-all',
|
|
}}>
|
|
{decodeHtmlEntities(device.name)}
|
|
</Table.Cell>
|
|
<Table.Cell>
|
|
<Box>{device.address}</Box>
|
|
<Box>
|
|
<Button
|
|
icon="pen"
|
|
onClick={() => {
|
|
act('copy', { 'copy': device.address });
|
|
act('switch_tab', { switch_tab: PHONTAB });
|
|
}}
|
|
content="Copy"
|
|
/>
|
|
<Button
|
|
icon="phone"
|
|
onClick={() => {
|
|
act('dial', { 'dial': device.address });
|
|
act('copy', { 'copy': device.address });
|
|
act('switch_tab', { switch_tab: PHONTAB });
|
|
}}
|
|
content="Call"
|
|
/>
|
|
<Button
|
|
icon="comment-alt"
|
|
onClick={() => {
|
|
act('copy', { 'copy': device.address });
|
|
act('copy_name', { 'copy_name': device.name });
|
|
act('switch_tab', { switch_tab: MESSSUBTAB });
|
|
}}
|
|
content="Msg"
|
|
/>
|
|
</Box>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
</Table>
|
|
)) || <Box>No devices detected on your local NTNet region.</Box>}
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[CONTTAB] = <ContactsTab />;
|
|
|
|
type MessagingTabData = {
|
|
// TAB
|
|
imContacts: { address: string; name: string }[];
|
|
|
|
// THREAD
|
|
targetAddressName: string;
|
|
targetAddress: string;
|
|
imList: { address: string; to_address: string; im: string }[];
|
|
};
|
|
|
|
/* Messaging */
|
|
const MessagingTab = (props, context) => {
|
|
const { act, data } = useBackend<MessagingTabData>(context);
|
|
|
|
const { imContacts } = data;
|
|
|
|
return (
|
|
<Section title="Messaging">
|
|
{(imContacts.length && (
|
|
<Table>
|
|
{imContacts.map((device) => (
|
|
<Table.Row key={device.address}>
|
|
<Table.Cell
|
|
color="label"
|
|
style={{
|
|
'word-break': 'break-all',
|
|
}}>
|
|
{decodeHtmlEntities(device.name)}:
|
|
</Table.Cell>
|
|
<Table.Cell>
|
|
<Box>{device.address}</Box>
|
|
<Box>
|
|
<Button
|
|
icon="comment"
|
|
onClick={() => {
|
|
act('copy', { copy: device.address });
|
|
act('copy_name', { copy_name: device.name });
|
|
act('switch_tab', { switch_tab: MESSSUBTAB });
|
|
}}
|
|
content="View Conversation"
|
|
/>
|
|
</Box>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
</Table>
|
|
)) || (
|
|
<Box>
|
|
You haven't sent any messages yet.
|
|
<Button fluid icon="user" onClick={() => act('switch_tab', { switch_tab: CONTTAB })} content="Contacts" />
|
|
</Box>
|
|
)}
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[MESSTAB] = <MessagingTab />;
|
|
|
|
/* Actual messaging conversation */
|
|
const IsIMOurs = (im, targetAddress) => {
|
|
return im.address !== targetAddress;
|
|
};
|
|
|
|
const enforceLengthLimit = (prefix: string, name: string, length: number) => {
|
|
if ((prefix + name).length > length) {
|
|
if (name.length > length) {
|
|
return name.slice(0, length) + '...';
|
|
}
|
|
return name;
|
|
}
|
|
return prefix + name;
|
|
};
|
|
|
|
const findClassMessage = (im, targetAddress, lastIndex, filterArray) => {
|
|
if (lastIndex < 0 || lastIndex > filterArray.length) {
|
|
return IsIMOurs(im, targetAddress) ? 'TinderMessage_First_Sent' : 'TinderMessage_First_Received';
|
|
}
|
|
|
|
let thisSent = IsIMOurs(im, targetAddress);
|
|
let lastSent = IsIMOurs(filterArray[lastIndex], targetAddress);
|
|
if (thisSent && lastSent) {
|
|
return 'TinderMessage_Subsequent_Sent';
|
|
} else if (!thisSent && !lastSent) {
|
|
return 'TinderMessage_Subsequent_Received';
|
|
}
|
|
return thisSent ? 'TinderMessage_First_Sent' : 'TinderMessage_First_Received';
|
|
};
|
|
|
|
const MessagingThreadTab = (props, context) => {
|
|
const { act, data } = useBackend<MessagingTabData>(context);
|
|
|
|
const { targetAddressName, targetAddress, imList } = data;
|
|
|
|
const [clipboardMode, setClipboardMode] = useLocalState(context, 'clipboardMode', false);
|
|
|
|
if (clipboardMode) {
|
|
return (
|
|
<Section
|
|
title={
|
|
<Box
|
|
inline
|
|
style={{
|
|
'white-space': 'nowrap',
|
|
'overflow-x': 'hidden',
|
|
}}
|
|
width="90%">
|
|
{enforceLengthLimit('Conversation with ', decodeHtmlEntities(targetAddressName), 30)}
|
|
</Box>
|
|
}
|
|
buttons={
|
|
<Button
|
|
icon="eye"
|
|
selected={clipboardMode}
|
|
tooltip="Exit Clipboard Mode"
|
|
tooltipPosition="bottom-end"
|
|
onClick={() => setClipboardMode(!clipboardMode)}
|
|
/>
|
|
}
|
|
height="100%"
|
|
stretchContents>
|
|
<Section
|
|
style={{
|
|
'height': '95%',
|
|
'overflow-y': 'auto',
|
|
}}>
|
|
{imList.map(
|
|
(im, i) =>
|
|
(im.to_address === targetAddress || im.address === targetAddress) && (
|
|
<Box
|
|
key={i}
|
|
className={IsIMOurs(im, targetAddress) ? 'ClassicMessage_Sent' : 'ClassicMessage_Received'}>
|
|
{IsIMOurs(im, targetAddress) ? 'You' : 'Them'}: {im.im}
|
|
</Box>
|
|
)
|
|
)}
|
|
</Section>
|
|
<Button icon="comment" onClick={() => act('message', { 'message': targetAddress })} content="Message" />
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Section
|
|
title={
|
|
<Box
|
|
inline
|
|
style={{
|
|
'white-space': 'nowrap',
|
|
'overflow-x': 'hidden',
|
|
}}
|
|
width="100%">
|
|
{enforceLengthLimit('Conversation with ', decodeHtmlEntities(targetAddressName), 30)}
|
|
</Box>
|
|
}
|
|
buttons={
|
|
<Button
|
|
icon="eye"
|
|
selected={clipboardMode}
|
|
tooltip="Enter Clipboard Mode"
|
|
tooltipPosition="bottom-end"
|
|
onClick={() => setClipboardMode(!clipboardMode)}
|
|
/>
|
|
}
|
|
height="100%"
|
|
stretchContents>
|
|
<Section
|
|
style={{
|
|
'height': '95%',
|
|
'overflow-y': 'auto',
|
|
}}>
|
|
{imList.map(
|
|
(im, i, filterArr) =>
|
|
(im.to_address === targetAddress || im.address === targetAddress) && (
|
|
<Box textAlign={IsIMOurs(im, targetAddress) ? 'right' : 'left'} mb={1} key={i}>
|
|
<Box maxWidth="75%" className={findClassMessage(im, targetAddress, i - 1, filterArr)} inline>
|
|
{decodeHtmlEntities(im.im)}
|
|
</Box>
|
|
</Box>
|
|
)
|
|
)}
|
|
</Section>
|
|
<Button icon="comment" onClick={() => act('message', { 'message': targetAddress })} content="Message" />
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[MESSSUBTAB] = <MessagingThreadTab />;
|
|
|
|
type NewsTabData = {
|
|
feeds: { index: number; name: string }[];
|
|
target_feed: { name: string; author: string; messages: NewsMessage[] };
|
|
latest_news: NewsMessage[];
|
|
};
|
|
|
|
type NewsMessage = {
|
|
ref: string;
|
|
body: string;
|
|
img: string;
|
|
caption: string;
|
|
message_type: string;
|
|
author: string;
|
|
time_stamp: string;
|
|
|
|
index: number;
|
|
channel: string;
|
|
};
|
|
|
|
/* News */
|
|
const NewsTab = (props, context) => {
|
|
const { act, data } = useBackend<NewsTabData>(context);
|
|
|
|
const { feeds, target_feed } = data;
|
|
|
|
return (
|
|
<Section title="News" stretchContents height="100%">
|
|
{(!feeds.length && <Box color="bad">Error: No newsfeeds available. Please try again later.</Box>) ||
|
|
(target_feed && <NewsTargetFeed />) || <NewsFeed />}
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
const NewsTargetFeed = (props, context) => {
|
|
const { act, data } = useBackend<NewsTabData>(context);
|
|
|
|
const { target_feed } = data;
|
|
|
|
return (
|
|
<Section
|
|
title={decodeHtmlEntities(target_feed.name) + ' by ' + decodeHtmlEntities(target_feed.author)}
|
|
buttons={<Button content="Back" icon="chevron-up" onClick={() => act('newsfeed', { newsfeed: null })} />}>
|
|
{target_feed.messages.map((message) => (
|
|
<Section key={message.ref}>
|
|
- {decodeHtmlEntities(message.body)}
|
|
{!!message.img && (
|
|
<Box>
|
|
<img src={'data:image/png;base64,' + message.img} />
|
|
{decodeHtmlEntities(message.caption) || null}
|
|
</Box>
|
|
)}
|
|
<Box color="grey">
|
|
[{message.message_type} by {decodeHtmlEntities(message.author)} - {message.time_stamp}]
|
|
</Box>
|
|
</Section>
|
|
))}
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
const NewsFeed = (props, context) => {
|
|
const { act, data } = useBackend<NewsTabData>(context);
|
|
|
|
const { feeds, latest_news } = data;
|
|
|
|
return (
|
|
<Fragment>
|
|
<Section title="Recent News">
|
|
<Section>
|
|
{latest_news.map((news) => (
|
|
<Box mb={2} key={news.index}>
|
|
<h5>
|
|
{decodeHtmlEntities(news.channel)}
|
|
<Button
|
|
ml={1}
|
|
icon="chevron-up"
|
|
onClick={() => act('newsfeed', { newsfeed: news.index })}
|
|
content="Go to"
|
|
/>
|
|
</h5>
|
|
- {decodeHtmlEntities(news.body)}
|
|
{!!news.img && (
|
|
<Box>
|
|
[image omitted, view story for more details]
|
|
{news.caption || null}
|
|
</Box>
|
|
)}
|
|
<Box fontSize={0.9}>
|
|
[{news.message_type} by{' '}
|
|
<Box inline color="average">
|
|
{news.author}
|
|
</Box>{' '}
|
|
- {news.time_stamp}]
|
|
</Box>
|
|
</Box>
|
|
))}
|
|
</Section>
|
|
</Section>
|
|
<Section title="News Feeds">
|
|
{feeds.map((feed) => (
|
|
<Button
|
|
key={feed.index}
|
|
fluid
|
|
icon="chevron-up"
|
|
onClick={() => act('newsfeed', { newsfeed: feed.index })}
|
|
content={feed.name}
|
|
/>
|
|
))}
|
|
</Section>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[NEWSTAB] = <NewsTab />;
|
|
|
|
type NoteTabData = {
|
|
note: string;
|
|
};
|
|
|
|
/* Note Keeper */
|
|
const NoteTab = (props, context) => {
|
|
const { act, data } = useBackend<NoteTabData>(context);
|
|
|
|
const { note } = data;
|
|
|
|
return (
|
|
<Section
|
|
title="Note Keeper"
|
|
height="100%"
|
|
stretchContents
|
|
buttons={<Button icon="pen" onClick={() => act('edit')} content="Edit Notes" />}>
|
|
<Section
|
|
color="average"
|
|
width="100%"
|
|
height="100%"
|
|
style={{
|
|
'word-break': 'break-all',
|
|
'overflow-y': 'auto',
|
|
}}>
|
|
{note}
|
|
</Section>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[NOTETAB] = <NoteTab />;
|
|
|
|
/* Weather App */
|
|
const getItemColor = (value, min2, min1, max1, max2) => {
|
|
if (value < min2) {
|
|
return 'bad';
|
|
} else if (value < min1) {
|
|
return 'average';
|
|
} else if (value > max1) {
|
|
return 'average';
|
|
} else if (value > max2) {
|
|
return 'bad';
|
|
}
|
|
return 'good';
|
|
};
|
|
|
|
type WeatherTabData = {
|
|
aircontents: AirContent[];
|
|
weather: Weather[];
|
|
};
|
|
|
|
type AirContent = {
|
|
entry: string;
|
|
val;
|
|
bad_low: number;
|
|
poor_low: number;
|
|
poor_high: number;
|
|
bad_high: number;
|
|
units;
|
|
};
|
|
|
|
type Weather = {
|
|
Planet: string;
|
|
Time: string;
|
|
Weather: string;
|
|
Temperature;
|
|
High;
|
|
Low;
|
|
WindDir;
|
|
WindSpeed;
|
|
Forecast: string;
|
|
};
|
|
|
|
const WeatherTab = (props, context) => {
|
|
const { act, data } = useBackend<WeatherTabData>(context);
|
|
|
|
const { aircontents, weather } = data;
|
|
|
|
return (
|
|
<Section title="Weather">
|
|
<Section title="Current Conditions">
|
|
<LabeledList>
|
|
{filter((i: AirContent) => i.val !== '0' || i.entry === 'Pressure' || i.entry === 'Temperature')(
|
|
aircontents
|
|
).map((item: AirContent) => (
|
|
<LabeledList.Item
|
|
key={item.entry}
|
|
label={item.entry}
|
|
color={getItemColor(item.val, item.bad_low, item.poor_low, item.poor_high, item.bad_high)}>
|
|
{item.val}
|
|
{decodeHtmlEntities(item.units)}
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
</Section>
|
|
<Section title="Weather Reports">
|
|
{(!!weather.length && (
|
|
<LabeledList>
|
|
{weather.map((wr) => (
|
|
<LabeledList.Item label={wr.Planet} key={wr.Planet}>
|
|
<LabeledList>
|
|
<LabeledList.Item label="Time">{wr.Time}</LabeledList.Item>
|
|
<LabeledList.Item label="Weather">{toTitleCase(wr.Weather)}</LabeledList.Item>
|
|
<LabeledList.Item label="Temperature">
|
|
Current: {wr.Temperature.toFixed()}°C | High: {wr.High.toFixed()}°C | Low:{' '}
|
|
{wr.Low.toFixed()}°C
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Wind Direction">{wr.WindDir}</LabeledList.Item>
|
|
<LabeledList.Item label="Wind Speed">{wr.WindSpeed}</LabeledList.Item>
|
|
<LabeledList.Item label="Forecast">{decodeHtmlEntities(wr.Forecast)}</LabeledList.Item>
|
|
</LabeledList>
|
|
</LabeledList.Item>
|
|
))}
|
|
</LabeledList>
|
|
)) || <Box color="bad">No weather reports available. Please check back later.</Box>}
|
|
</Section>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[WTHRTAB] = <WeatherTab />;
|
|
|
|
/* Crew Manifest */
|
|
// Lol just steal it from the existing template
|
|
TabToTemplate[MANITAB] = <CrewManifestContent />;
|
|
|
|
type SettingsTabData = {
|
|
owner: string;
|
|
occupation: string;
|
|
connectionStatus: BooleanLike;
|
|
address: string;
|
|
visible: BooleanLike;
|
|
ring: BooleanLike;
|
|
selfie_mode: BooleanLike;
|
|
};
|
|
|
|
/* Settings */
|
|
const SettingsTab = (props, context) => {
|
|
const { act, data } = useBackend<SettingsTabData>(context);
|
|
|
|
const { owner, occupation, connectionStatus, address, visible, ring, selfie_mode } = data;
|
|
|
|
return (
|
|
<Section title="Settings">
|
|
<LabeledList>
|
|
<LabeledList.Item label="Owner">
|
|
<Button icon="pen" fluid content={decodeHtmlEntities(owner)} onClick={() => act('rename')} />
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Camera Mode">
|
|
<Button
|
|
fluid
|
|
content={selfie_mode ? 'Front-facing Camera' : 'Rear-facing Camera'}
|
|
onClick={() => act('selfie_mode')}
|
|
/>
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Occupation">{decodeHtmlEntities(occupation)}</LabeledList.Item>
|
|
<LabeledList.Item label="Connection">
|
|
{connectionStatus === 1 ? <Box color="good">Connected</Box> : <Box color="bad">Disconnected</Box>}
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Device EPv2 Address">{address}</LabeledList.Item>
|
|
<LabeledList.Item label="Visibility">
|
|
<Button.Checkbox
|
|
checked={visible}
|
|
selected={visible}
|
|
fluid
|
|
content={
|
|
visible ? 'This device can be seen by other devices.' : 'This device is invisible to other devices.'
|
|
}
|
|
onClick={() => act('toggle_visibility')}
|
|
/>
|
|
</LabeledList.Item>
|
|
<LabeledList.Item label="Ringer">
|
|
<Button.Checkbox
|
|
checked={ring}
|
|
selected={ring}
|
|
fluid
|
|
content={ring ? 'Ringer on.' : 'Ringer off.'}
|
|
onClick={() => act('toggle_ringer')}
|
|
/>
|
|
<Button fluid content="Set Ringer Tone" onClick={() => act('set_ringer_tone')} />
|
|
</LabeledList.Item>
|
|
</LabeledList>
|
|
</Section>
|
|
);
|
|
};
|
|
|
|
TabToTemplate[SETTTAB] = <SettingsTab />;
|