Merge branch 'master' into test-predicate

This commit is contained in:
Jared Tobin 2019-08-09 06:53:36 -02:30
commit 96b8b3fe98
No known key found for this signature in database
GPG Key ID: 0E4647D58F8A69E4
13 changed files with 433 additions and 165 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2317,7 +2317,15 @@
url+(crip (apix:en-purl:html url.sep)) url+(crip (apix:en-purl:html url.sep))
:: ::
$exp $exp
mor+~[txt+"# {(trip exp.sep)}" tan+res.sep] =/ texp=tape ['>' ' ' (trip exp.sep)]
:- %mor
|- ^- (list sole-effect:sole-sur)
?: =("" texp) [tan+res.sep ~]
=/ newl (find "\0a" texp)
?~ newl [txt+texp $(texp "")]
=+ (trim u.newl texp)
:- txt+(scag u.newl texp)
$(texp [' ' ' ' (slag +(u.newl) texp)])
:: ::
$ire $ire
=+ num=(~(get by known) top.sep) =+ num=(~(get by known) top.sep)
@ -2402,7 +2410,11 @@
== ==
:: ::
$exp $exp
:- (tr-chow wyd '#' ' ' (trip exp.sep)) =+ texp=(trip exp.sep)
=+ newline=(find "\0a" texp)
=? texp ?=(^ newline)
(weld (scag u.newline texp) " ...")
:- (tr-chow wyd '#' ' ' texp)
?~ res.sep ~ ?~ res.sep ~
=- [' ' (tr-chow (dec wyd) ' ' -)]~ =- [' ' (tr-chow (dec wyd) ' ' -)]~
~(ram re (snag 0 `(list tank)`res.sep)) ~(ram re (snag 0 `(list tank)`res.sep))

View File

@ -65,7 +65,7 @@
++ lank ::: tank as string arr ++ lank ::: tank as string arr
|= a/tank |= a/tank
^- json ^- json
a+(turn (wash [0 1.024] a) tape) a+(turn (wash [0 80] a) tape)
:: ::
++ dank ::: tank ++ dank ::: tank
|= a/tank |= a/tank
@ -577,7 +577,7 @@
:+ ~ u.exp :+ ~ u.exp
=+ res=((ot res+(ar dank) ~) a) =+ res=((ot res+(ar dank) ~) a)
?^ res u.res ?^ res u.res
p:(mule |.([(sell (slap !>(..zuse) (ream u.exp)))]~)) ::TODO oldz p:(mule |.([(sell (slap !>(..^zuse) (ream u.exp)))]~)) ::TODO oldz
:: ::
++ atta ::: attache ++ atta ::: attache
^- $-(json (unit attache)) ^- $-(json (unit attache))

View File

@ -15904,7 +15904,6 @@
|* tem=rule |* tem=rule
%- star %- star
;~ pose ;~ pose
whit
;~(pfix bas tem) ;~(pfix bas tem)
;~(less tem prn) ;~(less tem prn)
== ==

View File

@ -208,12 +208,11 @@
%give %give
%response %response
%start %start
:- 404 ::
:~ ['content-type' 'text/html'] %+ complete-http-start-event
['content-length' '156'] :- 404
== ['content-type' 'text/html']~
[~ (error-page:http-server-gate 404 %.n '/' ~)] [~ (error-page:http-server-gate 404 %.n '/' ~)]
complete=%.y
== == == ==
== ==
:: ::
@ -384,12 +383,11 @@
^= expected-move ^= expected-move
:~ :* duct=~[/http-blah] %give %response :~ :* duct=~[/http-blah] %give %response
%start %start
:- 500 ::
:~ ['content-type' 'text/html'] %+ complete-http-start-event
['content-length' '180'] :- 500
== ['content-type' 'text/html']~
[~ (internal-server-error:http-server-gate %.n '/' ~)] [~ (internal-server-error:http-server-gate %.n '/' ~)]
complete=%.y
== == == == == ==
:: ::
;: weld ;: weld
@ -736,14 +734,13 @@
== ==
^= expected-move ^= expected-move
:~ :* duct=~[/http-blah] %give %response :~ :* duct=~[/http-blah] %give %response
:* %start %start
:- 200 ::
:~ ['content-type' 'text/plain'] %+ complete-http-start-event
['content-length' '13'] :- 200
== ['content-type' 'text/plain']~
`[13 'one two three'] `[13 'one two three']
%.y == == ==
== == == ==
:: ::
;: weld ;: weld
results1 results1
@ -870,11 +867,10 @@
%give %give
%response %response
%start %start
:- 403
:~ ['content-type' 'text/html']
['content-length' '182']
==
:: ::
%+ complete-http-start-event
:- 403
['content-type' 'text/html']~
:- ~ :- ~
%- error-page:http-server-gate :* %- error-page:http-server-gate :*
403 403
@ -882,8 +878,6 @@
'/~/channel/1234567890abcdef' '/~/channel/1234567890abcdef'
~ ~
== ==
::
complete=%.y
== == == ==
== ==
:: ::
@ -2050,12 +2044,11 @@
%give %give
%response %response
%start %start
:- 200 ::
:~ ['content-type' 'text/html'] %+ complete-http-start-event
['content-length' '1752'] :- 200
== ['content-type' 'text/html']~
[~ (login-page:http-server-gate `'/~landscape/inner-path' ~nul)] [~ (login-page:http-server-gate `'/~landscape/inner-path' ~nul)]
complete=%.y
== == == ==
== ==
:: a response post redirects back to the application, setting cookie :: a response post redirects back to the application, setting cookie
@ -2201,4 +2194,12 @@
:: This is the default code for a fakeship. :: This is the default code for a fakeship.
:: ::
[~ ~ %noun !>(.~lidlut-tabwed-savheb-loslux)] [~ ~ %noun !>(.~lidlut-tabwed-savheb-loslux)]
:: produce the body of a %start http-event with the correct content-length
::
++ complete-http-start-event
|= [response-header:http data=(unit octs)]
=- [[status-code -] data %.y]
?~ data headers
%+ weld headers
['content-length' (crip ((d-co:co 1) p.u.data))]~
-- --

View File

@ -88,6 +88,14 @@ h2 {
font-weight: bold; font-weight: bold;
} }
.fs-italic {
font-style: italic;
}
.td-underline {
text-decoration: underline;
}
.bg-v-light-gray { .bg-v-light-gray {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
@ -116,6 +124,16 @@ h2 {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.clamp-message {
max-width: calc(100% - 36px - 1.5rem);
}
.clamp-attachment {
overflow: scroll;
max-height: 10em;
max-width: 100%;
}
.lh-16 { .lh-16 {
line-height: 16px; line-height: 16px;
} }

View File

@ -226,8 +226,8 @@ export class ChatScreen extends Component {
numPeers={peers.length} /> numPeers={peers.length} />
</div> </div>
<div <div
className="overflow-y-scroll pt3 flex flex-column-reverse" className="overflow-y-scroll pt3 pb2 flex flex-column-reverse"
style={{ height: 'calc(100% - 157px)' }} style={{ height: 'calc(100% - 157px)', resize: 'vertical' }}
onScroll={this.onScroll}> onScroll={this.onScroll}>
<div ref={ el => { this.scrollElement = el; }}></div> <div ref={ el => { this.scrollElement = el; }}></div>
{chatMessages} {chatMessages}

View File

@ -54,7 +54,9 @@ export class ChatInput extends Component {
setTimeout(closure, 2000);*/ setTimeout(closure, 2000);*/
this.state = { this.state = {
message: "" message: '',
messageType: 'lin',
clipboard: null
}; };
this.textareaRef = React.createRef(); this.textareaRef = React.createRef();
@ -100,22 +102,165 @@ export class ChatInput extends Component {
} }
messageChange(event) { messageChange(event) {
this.setState({message: event.target.value}); const input = event.target.value;
const previous = this.state.message;
//NOTE dumb hack to work around paste event flow oddities
const pasted = (previous.length === 0 && input.length > 1);
if (input !== this.state.clipboard) {
this.setState({
message: input,
messageType: this.getSpeechType(input),
clipboard: (pasted ? input : null)
});
}
}
getSpeechType(input) {
if (input[0] === '#') {
return 'exp';
} else if (input.indexOf('\n') >= 0) {
return 'fat';
} else if (input[0] === '@') {
return 'lin@';
} else if (this.isUrl(input)) {
return 'url';
} else {
return 'lin';
}
}
getSpeechStyle(type, clipboard) {
switch (type) {
case 'lin@':
return 'fs-italic';
case 'url':
return 'td-underline';
case 'exp':
return 'code';
case 'fat':
if (clipboard) return 'code';
default:
return '';
}
}
isUrl(string) {
try {
const urlObject = new URL(string);
//NOTE we check for a host to ensure a url is actually being posted
// to combat false positives for things like "marzod: ur cool".
// this does mean you can't send "mailto:e@ma.il" as %url message,
// but the desirability of that seems questionable anyway.
return (urlObject.host !== '');
} catch (e) {
return false;
}
}
// turns select urls into arvo:// urls
//
// we detect app names from the url. if the app is known to handle requests
// for remote data (instead of serving only from the host) we transfor the
// url into a generic arvo:// one.
// the app name format is pretty distinct and rare to find in the non-urbit
// wild, but this could still result in false positives for older-school
// websites serving pages under /~user paths.
// we could match only on ship.arvo.network, but that would exclude those
// running on localhost or under a custom domain.
//
//
globalizeUrl(url) {
const urlObject = new URL(url);
const app = urlObject.pathname.split('/')[1];
if (app === '~chat' ||
app === '~publish') {
//TODO send proper url speeches once hall starts using a url type that
// supports non-http protocols.
return { lin: {
msg: 'arvo://' + url.slice(urlObject.origin.length),
pat: false
} };
} else {
return {url};
}
}
speechFromInput(content, type, clipboard) {
switch (type) {
case 'lin':
return { lin: {
msg: content,
pat: false
} };
//
case 'lin@':
return { lin: {
msg: content.slice(1),
pat: true
} };
//
case 'url':
return this.globalizeUrl(content);
//
case 'exp':
// remove leading #
content = content.slice(1);
// remove insignificant leading whitespace.
// aces might be relevant to style.
while (content[0] === '\n') {
content = content.slice(1);
}
return { exp: {
exp: content
} };
//
case 'fat':
// clipboard contents
if (clipboard !== null) {
return { fat: {
sep: { lin: { msg: '', pat: false } },
tac: { name: {
nom: 'clipboard',
tac: { text: content }
} }
} };
// long-form message
} else {
const lines = content.split('\n');
return { fat: {
sep: { lin: {
msg: lines[0],
pat: false
} },
tac: { name: {
nom: 'long-form',
tac: { text: lines.slice(1).join('\n') }
} },
} };
}
//
default:
throw new Error('Unimplemented speech type', type);
}
} }
messageSubmit() { messageSubmit() {
const { props, state } = this; const { props, state } = this;
if (state.message === '') {
return;
}
let message = { let message = {
uid: uuid(), uid: uuid(),
aut: window.ship, aut: window.ship,
wen: Date.now(), wen: Date.now(),
aud: [props.station], aud: [props.station],
sep: { sep: this.speechFromInput(
lin: { state.message,
msg: state.message, state.messageType,
pat: false state.clipboard
} )
}
}; };
props.api.hall( props.api.hall(
@ -125,7 +270,8 @@ export class ChatInput extends Component {
); );
this.setState({ this.setState({
message: "" message: '',
messageType: 'lin'
}); });
} }
@ -151,7 +297,7 @@ export class ChatInput extends Component {
} }
return ( return (
<div className="mt2 pa3 cf flex black bt b--black-30"> <div className="pa3 cf flex black bt b--black-30" style={{ flexGrow: 1 }}>
<div className="fl" style={{ <div className="fl" style={{
marginTop: 4, marginTop: 4,
flexBasis: 32, flexBasis: 32,
@ -159,9 +305,12 @@ export class ChatInput extends Component {
}}> }}>
<Sigil ship={window.ship} size={32} /> <Sigil ship={window.ship} size={32} />
</div> </div>
<div className="fr h-100 flex" style={{ flexGrow: 1, height: 40 }}> <div className="fr h-100 flex" style={{ flexGrow: 1 }}>
<input className="ml2 bn" <textarea
style={{ flexGrow: 1, height: 40 }} className={'ml2 mt2 mr2 bn ' +
this.getSpeechStyle(state.messageType, state.clipboard)
}
style={{ flexGrow: 1, height: 40, resize: 'none' }}
ref={this.textareaRef} ref={this.textareaRef}
placeholder={props.placeholder} placeholder={props.placeholder}
value={state.message} value={state.message}

View File

@ -5,62 +5,168 @@ import moment from 'moment';
import _ from 'lodash'; import _ from 'lodash';
export class Message extends Component { export class Message extends Component {
renderMessage(content) { renderSpeech(speech) {
if (_.has(speech, 'lin')) {
return this.renderLin(speech.lin.msg, speech.lin.pat);
} else if (_.has(speech, 'url')) {
return this.renderUrl(speech.url);
} else if (_.has(speech, 'exp')) {
return this.renderExp(speech.exp.exp, speech.exp.res);
} else if (_.has(speech, 'ire')) {
return this.renderSpeech(speech.ire.sep);
} else if (_.has(speech, 'app')) {
return this.renderSpeech(speech.app.sep);
} else if (_.has(speech, 'fat')) {
return this.renderFat(speech.fat.sep, speech.fat.tac);
} else {
return this.renderUnknown();
}
}
renderUnknown() {
return this.renderLin('<unknown message type>')
}
renderLin(content, action = false) {
if (content === '') {
return null;
}
//TODO remove once arvo:// urls are supported in url speeches
if (content.indexOf('arvo://') === 0) {
return this.renderUrl(content);
}
return ( return (
<p className="body-regular-400 v-top"> <p className={`body-regular-400 v-top ${action ? 'fs-italic' : ''}`}>
{content} {content}
</p> </p>
); );
} }
renderContent() { renderUrl(url) {
const { props } = this;
let content = _.get(
props.msg,
'sep.lin.msg',
'<unknown message type>'
);
try { try {
let url = new URL(content); let urlObject = new URL(url);
let imgMatch = let imgMatch =
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/ /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/
.exec( .exec(
url.pathname urlObject.pathname
); );
if (imgMatch) { if (imgMatch) {
return ( return this.renderImageUrl(url);
<img
src={content}
style={{
width:"50%",
maxWidth: '250px'
}}
></img>
)
} else { } else {
let url = this.urlTransmogrifier(content); let localUrl = this.localizeUrl(url);
return this.renderAnchor(localUrl, url);
return (
<a className="body-regular"
href={url}
target="_blank">{url}</a>
)
} }
} catch(e) { } catch(e) {
return this.renderMessage(content); console.error('url render error', e);
return this.renderAnchor(url);
} }
} }
urlTransmogrifier(url) { renderImageUrl(url) {
if (typeof url !== 'string') { throw 'Only transmogrify strings!'; } return this.renderAnchor(url, (
const ship = window.ship; <img
if (url.indexOf('arvo://') === 0) { src={url}
return url.split('arvo://')[1]; style={{
width:"50%",
maxWidth: '250px'
}}
></img>
));
}
renderAnchor(href, content) {
content = content || href;
return (
<a className="body-regular"
href={href}
target="_blank">{content}</a>
);
}
renderExp(expression, result) {
return (<>
<p>
<pre className="clamp-attachment pa1 mt0 mb0 bg-light-gray">
{expression}
</pre>
<pre className="clamp-attachment pa1 mt0 mb0">
{result[0].join('\n')}
</pre>
</p>
</>);
}
renderFat(speech, attachment) {
return (<>
{this.renderSpeech(speech)}
{this.renderAttachment(attachment)}
</>);
}
renderAttachment(content, title = '') {
if (_.has(content, 'name')) {
return this.renderAttachment(content.name.tac, content.name.nom);
}
return (<details>
<summary className="inter fs-italic">{'Attached: ' + title}</summary>
{ _.has(content, 'text')
? (title === 'long-form')
? this.renderParagraphs(content.text.split('\n'))
: this.renderPlaintext(content.text)
: _.has(content, 'tank')
? this.renderPlaintext(content.tank.join('\n'))
: null
}
</details>);
}
renderParagraphs(paragraphs) {
return (<div className="clamp-attachment">
{paragraphs.map(p => (<p className="mt2">{p}</p>))}
</div>);
}
renderPlaintext(text) {
return (<pre className="clamp-attachment">{text}</pre>);
}
renderContent() {
const { props } = this;
try {
if (!_.has(props.msg, 'sep')) {
return this.renderUnknown();
}
return this.renderSpeech(props.msg.sep);
} catch (e) {
console.error('speech rendering error', e);
return this.renderUnknown();
}
}
renderAuthor() {
const msg = this.props.msg;
const ship = '~' + msg.aut;
if (_.has(msg, 'sep.app.app')) {
return `:${msg.sep.app.app} (${ship})`;
} else {
return ship;
}
}
//NOTE see also lib/chat-input's globalizeUrl
localizeUrl(url) {
if (typeof url !== 'string') { throw 'Only localize strings!'; }
const arvo = 'arvo://';
if (url.indexOf(arvo) === 0) {
// this application is being served by an urbit also, so /path will
// point to the arvo url as hosted by this same urbit.
return url.slice(arvo.length);
} else {
return url;
} }
return url;
} }
render() { render() {
@ -82,10 +188,10 @@ export class Message extends Component {
<div className="fl mr2"> <div className="fl mr2">
<Sigil ship={props.msg.aut} size={36} /> <Sigil ship={props.msg.aut} size={36} />
</div> </div>
<div className="fr" style={{ flexGrow: 1, marginTop: -8 }}> <div className="fr clamp-message" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child"> <div className="hide-child">
<p className="v-top label-small-mono gray dib mr3"> <p className="v-top label-small-mono gray dib mr3">
~{props.msg.aut} {this.renderAuthor()}
</p> </p>
<p className="v-top label-small-mono gray dib">{timestamp}</p> <p className="v-top label-small-mono gray dib">{timestamp}</p>
<p className="v-top label-small-mono ml2 gray dib child"> <p className="v-top label-small-mono ml2 gray dib child">
@ -105,12 +211,11 @@ export class Message extends Component {
minHeight: 'min-content' minHeight: 'min-content'
}}> }}>
<p className="child pl3 pr2 label-small-mono gray dib">{timestamp}</p> <p className="child pl3 pr2 label-small-mono gray dib">{timestamp}</p>
<div className="fr" style={{ flexGrow: 1 }}> <div className="fr clamp-message" style={{ flexGrow: 1 }}>
{this.renderContent()} {this.renderContent()}
</div> </div>
</div> </div>
) )
} }
} }
} }

View File

@ -61,7 +61,7 @@ export class Root extends Component {
let internalStation = host + '/hall-internal-' + circle; let internalStation = host + '/hall-internal-' + circle;
if (internalStation in state.configs) { if (internalStation in state.configs) {
unreads[cir] = unreads[cir] =
state.configs[internalStation].red <= state.configs[internalStation].red <=
messages[cir][messages[cir].length - 1].num; messages[cir][messages[cir].length - 1].num;
} else { } else {
@ -87,6 +87,18 @@ export class Root extends Component {
inviteConfig = configs[`~${window.ship}/i`]; inviteConfig = configs[`~${window.ship}/i`];
} }
const renderChannelsSidebar = (props) => (
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
);
return ( return (
<BrowserRouter> <BrowserRouter>
<div> <div>
@ -94,17 +106,7 @@ export class Root extends Component {
render={ (props) => { render={ (props) => {
return ( return (
<Skeleton <Skeleton
sidebar={ sidebar={renderChannelsSidebar(props)}>
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<div className="w-100 h-100 fr" style={{ flexGrow: 1 }}> <div className="w-100 h-100 fr" style={{ flexGrow: 1 }}>
<div className="dt w-100 h-100"> <div className="dt w-100 h-100">
<div className="dtc center v-mid w-100 h-100 bg-white"> <div className="dtc center v-mid w-100 h-100 bg-white">
@ -119,18 +121,8 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={this.state.spinner} spinner={this.state.spinner}
sidebar={ sidebar={renderChannelsSidebar(props)}>
<Sidebar <NewScreen
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<NewScreen
setSpinner={this.setSpinner} setSpinner={this.setSpinner}
api={api} api={api}
circles={circles} circles={circles}
@ -143,17 +135,7 @@ export class Root extends Component {
render={ (props) => { render={ (props) => {
return ( return (
<Skeleton <Skeleton
sidebar={ sidebar={renderDefaultSidebar(props)}>
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<LandingScreen <LandingScreen
api={api} api={api}
configs={configs} configs={configs}
@ -164,24 +146,14 @@ export class Root extends Component {
}} /> }} />
<Route exact path="/~chat/:ship/:station" <Route exact path="/~chat/:ship/:station"
render={ (props) => { render={ (props) => {
let station = let station =
props.match.params.ship props.match.params.ship
+ "/" + + "/" +
props.match.params.station; props.match.params.station;
let messages = state.messages[station] || []; let messages = state.messages[station] || [];
return ( return (
<Skeleton <Skeleton
sidebar={ sidebar={renderChannelsSidebar(props) }>
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<ChatScreen <ChatScreen
api={api} api={api}
configs={configs} configs={configs}
@ -197,19 +169,9 @@ export class Root extends Component {
render={ (props) => { render={ (props) => {
return ( return (
<Skeleton <Skeleton
sidebar={ sidebar={renderChannelsSidebar(props) }>
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<MemberScreen <MemberScreen
{...props} {...props}
api={api} api={api}
peers={state.peers} peers={state.peers}
/> />
@ -221,18 +183,8 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={this.state.spinner} spinner={this.state.spinner}
sidebar={ sidebar={renderChannelsSidebar(props) }>
<Sidebar <SettingsScreen
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<SettingsScreen
{...props} {...props}
setSpinner={this.setSpinner} setSpinner={this.setSpinner}
api={api} api={api}

View File

@ -83,6 +83,30 @@ export class Sidebar extends Component {
this.props.history.push('/~chat/new'); this.props.history.push('/~chat/new');
} }
summarizeMessage(speech) {
const fallback = '...';
if (_.has(speech, 'lin')) {
return speech.lin.msg;
} else if (_.has(speech, 'url')) {
return speech.url;
} else if (_.has(speech, 'exp')) {
return '# ' + speech.exp.exp;
} else if (_.has(speech, 'ire')) {
return this.summarizeMessage(speech.ire.sep);
} else if (_.has(speech, 'app')) {
return this.summarizeMessage(speech.app.sep);
} else if (_.has(speech, 'fat')) {
const msg = this.summarizeMessage(speech.fat.sep);
if (msg !== '' && msg !== fallback) return msg;
return 'Attachment' +
(_.has(speech, 'fat.tac.name.nom')
? ': ' + speech.fat.tac.name.nom
: '');
} else {
return fallback;
}
}
render() { render() {
const { props, state } = this; const { props, state } = this;
let station = props.match.params.ship + '/' + props.match.params.station; let station = props.match.params.ship + '/' + props.match.params.station;
@ -93,7 +117,9 @@ export class Sidebar extends Component {
}) })
.map((cir) => { .map((cir) => {
let msg = props.messagePreviews[cir]; let msg = props.messagePreviews[cir];
let content = _.get(msg, 'gam.sep.lin.msg', 'No messages yet'); let content = _.has(msg, 'gam.sep')
? this.summarizeMessage(msg.gam.sep)
: 'No messages yet';
let aut = !!msg ? msg.gam.aut : ''; let aut = !!msg ? msg.gam.aut : '';
let wen = !!msg ? msg.gam.wen : 0; let wen = !!msg ? msg.gam.wen : 0;
let datetime = let datetime =

View File

@ -23,6 +23,12 @@ pre {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
code {
padding: 8px;
background-color: #f9f9f9;
white-space: pre-wrap;
}
a { a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;