mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 10:43:20 +00:00
1320 lines
35 KiB
TypeScript
1320 lines
35 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;
|
|
|
|
let deg = '\u00B0';
|
|
|
|
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()} {deg}C | High:{' '}
|
|
{wr.High.toFixed()} {deg}C | Low: {wr.Low.toFixed()} {deg}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 />;
|