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))
::
$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
=+ num=(~(get by known) top.sep)
@ -2402,7 +2410,11 @@
==
::
$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 ~
=- [' ' (tr-chow (dec wyd) ' ' -)]~
~(ram re (snag 0 `(list tank)`res.sep))

View File

@ -65,7 +65,7 @@
++ lank ::: tank as string arr
|= a/tank
^- json
a+(turn (wash [0 1.024] a) tape)
a+(turn (wash [0 80] a) tape)
::
++ dank ::: tank
|= a/tank
@ -577,7 +577,7 @@
:+ ~ u.exp
=+ res=((ot res+(ar dank) ~) a)
?^ 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
^- $-(json (unit attache))

View File

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

View File

@ -208,12 +208,11 @@
%give
%response
%start
:- 404
:~ ['content-type' 'text/html']
['content-length' '156']
==
::
%+ complete-http-start-event
:- 404
['content-type' 'text/html']~
[~ (error-page:http-server-gate 404 %.n '/' ~)]
complete=%.y
== ==
==
::
@ -384,12 +383,11 @@
^= expected-move
:~ :* duct=~[/http-blah] %give %response
%start
:- 500
:~ ['content-type' 'text/html']
['content-length' '180']
==
::
%+ complete-http-start-event
:- 500
['content-type' 'text/html']~
[~ (internal-server-error:http-server-gate %.n '/' ~)]
complete=%.y
== == ==
::
;: weld
@ -736,14 +734,13 @@
==
^= expected-move
:~ :* duct=~[/http-blah] %give %response
:* %start
:- 200
:~ ['content-type' 'text/plain']
['content-length' '13']
==
`[13 'one two three']
%.y
== == == ==
%start
::
%+ complete-http-start-event
:- 200
['content-type' 'text/plain']~
`[13 'one two three']
== == ==
::
;: weld
results1
@ -870,11 +867,10 @@
%give
%response
%start
:- 403
:~ ['content-type' 'text/html']
['content-length' '182']
==
::
%+ complete-http-start-event
:- 403
['content-type' 'text/html']~
:- ~
%- error-page:http-server-gate :*
403
@ -882,8 +878,6 @@
'/~/channel/1234567890abcdef'
~
==
::
complete=%.y
== ==
==
::
@ -2050,12 +2044,11 @@
%give
%response
%start
:- 200
:~ ['content-type' 'text/html']
['content-length' '1752']
==
::
%+ complete-http-start-event
:- 200
['content-type' 'text/html']~
[~ (login-page:http-server-gate `'/~landscape/inner-path' ~nul)]
complete=%.y
== ==
==
:: a response post redirects back to the application, setting cookie
@ -2201,4 +2194,12 @@
:: This is the default code for a fakeship.
::
[~ ~ %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;
}
.fs-italic {
font-style: italic;
}
.td-underline {
text-decoration: underline;
}
.bg-v-light-gray {
background-color: #f9f9f9;
}
@ -116,6 +124,16 @@ h2 {
-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 {
line-height: 16px;
}

View File

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

View File

@ -54,7 +54,9 @@ export class ChatInput extends Component {
setTimeout(closure, 2000);*/
this.state = {
message: ""
message: '',
messageType: 'lin',
clipboard: null
};
this.textareaRef = React.createRef();
@ -100,22 +102,165 @@ export class ChatInput extends Component {
}
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() {
const { props, state } = this;
if (state.message === '') {
return;
}
let message = {
uid: uuid(),
aut: window.ship,
wen: Date.now(),
aud: [props.station],
sep: {
lin: {
msg: state.message,
pat: false
}
}
sep: this.speechFromInput(
state.message,
state.messageType,
state.clipboard
)
};
props.api.hall(
@ -125,7 +270,8 @@ export class ChatInput extends Component {
);
this.setState({
message: ""
message: '',
messageType: 'lin'
});
}
@ -151,7 +297,7 @@ export class ChatInput extends Component {
}
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={{
marginTop: 4,
flexBasis: 32,
@ -159,9 +305,12 @@ export class ChatInput extends Component {
}}>
<Sigil ship={window.ship} size={32} />
</div>
<div className="fr h-100 flex" style={{ flexGrow: 1, height: 40 }}>
<input className="ml2 bn"
style={{ flexGrow: 1, height: 40 }}
<div className="fr h-100 flex" style={{ flexGrow: 1 }}>
<textarea
className={'ml2 mt2 mr2 bn ' +
this.getSpeechStyle(state.messageType, state.clipboard)
}
style={{ flexGrow: 1, height: 40, resize: 'none' }}
ref={this.textareaRef}
placeholder={props.placeholder}
value={state.message}

View File

@ -5,62 +5,168 @@ import moment from 'moment';
import _ from 'lodash';
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 (
<p className="body-regular-400 v-top">
<p className={`body-regular-400 v-top ${action ? 'fs-italic' : ''}`}>
{content}
</p>
);
}
renderContent() {
const { props } = this;
let content = _.get(
props.msg,
'sep.lin.msg',
'<unknown message type>'
);
renderUrl(url) {
try {
let url = new URL(content);
let imgMatch =
let urlObject = new URL(url);
let imgMatch =
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/
.exec(
url.pathname
urlObject.pathname
);
if (imgMatch) {
return (
<img
src={content}
style={{
width:"50%",
maxWidth: '250px'
}}
></img>
)
return this.renderImageUrl(url);
} else {
let url = this.urlTransmogrifier(content);
return (
<a className="body-regular"
href={url}
target="_blank">{url}</a>
)
let localUrl = this.localizeUrl(url);
return this.renderAnchor(localUrl, url);
}
} catch(e) {
return this.renderMessage(content);
console.error('url render error', e);
return this.renderAnchor(url);
}
}
urlTransmogrifier(url) {
if (typeof url !== 'string') { throw 'Only transmogrify strings!'; }
const ship = window.ship;
if (url.indexOf('arvo://') === 0) {
return url.split('arvo://')[1];
renderImageUrl(url) {
return this.renderAnchor(url, (
<img
src={url}
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() {
@ -82,10 +188,10 @@ export class Message extends Component {
<div className="fl mr2">
<Sigil ship={props.msg.aut} size={36} />
</div>
<div className="fr" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="fr clamp-message" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child">
<p className="v-top label-small-mono gray dib mr3">
~{props.msg.aut}
{this.renderAuthor()}
</p>
<p className="v-top label-small-mono gray dib">{timestamp}</p>
<p className="v-top label-small-mono ml2 gray dib child">
@ -105,12 +211,11 @@ export class Message extends Component {
minHeight: 'min-content'
}}>
<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()}
</div>
</div>
)
}
}
}

View File

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

View File

@ -83,6 +83,30 @@ export class Sidebar extends Component {
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() {
const { props, state } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
@ -93,7 +117,9 @@ export class Sidebar extends Component {
})
.map((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 wen = !!msg ? msg.gam.wen : 0;
let datetime =

View File

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