Merge branch 'release/next-userspace' into lf/settings-screen

This commit is contained in:
Liam Fitzgerald 2020-08-17 18:10:00 +10:00
commit e89cb3bd71
204 changed files with 1248 additions and 1246 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d7088528dbfd54a913921ade093251d678c4ccebfd0ad85ef2022520266b3954
size 16451173
oid sha256:ef417c3092dc32d6d5897a7ba63f3f8910f928f0aa23adf3a356b88ce027a415
size 6260173

View File

@ -1,7 +1,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v2.ssh1k.1s91p.s70g6.j21r5.hr9lc
++ hash 0v7.foe2o.ang8k.28dnr.fudi0.74c8d
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -4,11 +4,11 @@
<title>OS1</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
<link rel="manifest"
href='data:application/manifest+json,{
@ -20,9 +20,9 @@
"theme_color": "%23000000"}' />
</head>
<body>
<div id="root"/>
<div id="root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.4257dcef75bd12c7c419.js"></script>
<script src="/~landscape/js/bundle/index.8e25dc41456a44c967da.js"></script>
</body>
</html>

View File

@ -1831,6 +1831,8 @@
::
%subscribe
?> (team:title our.bol src.bol)
?: =(our.bol who.act)
[~ state]
=/ join-wire=wire
/join-group/[(scot %p who.act)]/[book.act]
=/ meta=(unit (set path))

14
pkg/interface/.babelrc Normal file
View File

@ -0,0 +1,14 @@
{
"plugins": [
[
"babel-plugin-root-import",
{
"paths": [
{
"rootPathSuffix": "./src"
}
]
}
]
]
}

View File

@ -2479,6 +2479,15 @@
"object.assign": "^4.1.0"
}
},
"babel-plugin-root-import": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/babel-plugin-root-import/-/babel-plugin-root-import-6.5.0.tgz",
"integrity": "sha512-PTD8fPl4v1kwn01u9d4rgRavDs5Z+jv4qa4/y6iYtoSgM4/xmjwMqo66j5A/BTZQEMA6OV5iFgyZ1PIhroJqqg==",
"dev": true,
"requires": {
"slash": "^3.0.0"
}
},
"babel-plugin-styled-components": {
"version": "1.10.7",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz",
@ -8623,6 +8632,12 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"slice-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",

View File

@ -55,6 +55,7 @@
"@typescript-eslint/parser": "^3.8.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-root-import": "^6.5.0",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2",
"eslint": "^6.8.0",

View File

@ -1,65 +0,0 @@
import React, { Component, Fragment } from "react";
import { Link } from "react-router-dom";
import { ChatTabBar } from "./chat-tabbar";
import { SidebarSwitcher } from "../../../../components/SidebarSwitch";
import { deSig } from "../../../../lib/util";
export class ChatHeader extends Component {
render() {
const { props } = this;
const isinPopout = props.popout ? "popout/" : "";
const group = Array.from(props.group.members);
let title = props.station.substr(1);
if (props.association &&
"metadata" in props.association &&
props.association.metadata.tile !== "") {
title = props.association.metadata.title
}
return (
<Fragment>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
to={"/~chat/" + isinPopout + "room" + props.station}
className="pt2 white-d"
>
<h2
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}
>
{title}
</h2>
</Link>
<ChatTabBar
location={props.location}
station={props.station}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={props.popout}
/>
</div>
</Fragment>
);
}
}

View File

@ -1,66 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class ChatTabBar extends Component {
render() {
const props = this.props;
let memColor = '',
setColor = '',
popout = '';
if (props.location.pathname.includes('/settings')) {
memColor = 'gray3';
setColor = 'black white-d';
} else if (props.location.pathname.includes('/members')) {
memColor = 'black white-d';
setColor = 'gray3';
} else {
memColor = 'gray3';
setColor = 'gray3';
}
popout = props.location.pathname.includes('/popout')
? 'popout/' : '';
const hidePopoutIcon = (this.props.popout)
? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl';
return (
<div className="dib flex-shrink-0 flex-grow-1">
{props.isOwner ? (
<div className={'dib pt2 f9 pl6 lh-solid'}>
<Link
className={'no-underline ' + memColor}
to={'/~chat/' + popout + 'members' + props.station}
>
Members
</Link>
</div>
) : (
<div className="dib" style={{ width: 0 }}></div>
)}
<div className={'dib pt2 f9 pl6 pr6 lh-solid'}>
<Link
className={'no-underline ' + setColor}
to={'/~chat/' + popout + 'settings' + props.station}
>
Settings
</Link>
</div>
<a href={'/~chat/popout/room' + props.station} rel="noopener noreferrer"
target="_blank"
className="dib fr pr1"
style={{ paddingTop: '8px' }}
>
<img
className={'flex-shrink-0 pr3 dn ' + hidePopoutIcon}
src="/~chat/img/popout.png"
height="16"
width="16"
/>
</a>
</div>
);
}
}

View File

@ -1,94 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { deSig } from '../../../lib/util';
import { ChatTabBar } from './lib/chat-tabbar';
import { MemberElement } from './lib/member-element';
import { InviteElement } from './lib/invite-element';
import { SidebarSwitcher } from '../../../components/SidebarSwitch';
import { GroupView } from '../../../components/Group';
import { PatpNoSig } from '../../../types/noun';
export class MemberScreen extends Component {
constructor(props) {
super(props);
this.inviteShips = this.inviteShips.bind(this);
}
inviteShips(ships) {
const { props } = this;
return props.api.chat.invite(props.station, ships.map(s => `~${s}`));
}
render() {
const { props } = this;
const isinPopout = this.props.popout ? 'popout/' : '';
let title = props.station.substr(1);
if (props.association && 'metadata' in props.association) {
title =
props.association.metadata.title !== ''
? props.association.metadata.title
: props.station.substr(1);
}
return (
<div className='h-100 w-100 overflow-x-hidden flex flex-column white-d'>
<div
className='w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8'
style={{ height: '1rem' }}
>
<Link to='/~chat/'>{'⟵ All Chats'}</Link>
</div>
<div
className={`pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative
overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0`}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
api={this.props.api}
/>
<Link
to={'/~chat/' + isinPopout + 'room' + props.station}
className='pt2 white-d'
>
<h2
className={
'dib f9 fw4 lh-solid v-top ' +
(title === props.station.substr(1) ? 'mono' : '')
}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={5}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={this.props.popout}
api={props.api}
/>
</div>
<div className='w-100 pl3 mt0 mt4-m mt4-l mt4-xl cf pr6'>
{ props.association['group-path'] && (
<GroupView
permissions
group={props.group}
resourcePath={props.association['group-path'] || ''}
associations={props.associations}
groups={props.groups}
inviteShips={this.inviteShips}
contacts={props.contacts}
/> )}
</div>
</div>
);
}
}

View File

@ -1,459 +0,0 @@
import React, { Component } from 'react';
import { deSig, uxToHex, writeText } from '../../../lib/util';
import { Link } from 'react-router-dom';
import { Spinner } from '../../../components/Spinner';
import { ChatTabBar } from './lib/chat-tabbar';
import { InviteSearch } from '../../../components/InviteSearch';
import SidebarSwitcher from '../../../components/SidebarSwitch';
import Toggle from '../../../components/toggle';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
title: '',
description: '',
color: '',
// groupify settings
targetGroup: null,
inclusive: false,
awaiting: false,
type: 'Editing chat...'
};
this.renderDelete = this.renderDelete.bind(this);
this.changeTargetGroup = this.changeTargetGroup.bind(this);
this.changeInclusive = this.changeInclusive.bind(this);
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
this.changeColor = this.changeColor.bind(this);
this.submitColor = this.submitColor.bind(this);
}
componentDidMount() {
const { props } = this;
if (props.association && 'metadata' in props.association) {
this.setState({
title: props.association.metadata.title,
description: props.association.metadata.description,
color: `#${uxToHex(props.association.metadata.color)}`
});
}
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (Boolean(state.isLoading) && !(props.station in props.inbox)) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~chat');
});
}
if ((state.title === '') && (prevProps !== props)) {
if (props.association && 'metadata' in props.association)
this.setState({
title: props.association.metadata.title,
description: props.association.metadata.description,
color: `#${uxToHex(props.association.metadata.color)}`
});
}
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
changeTitle() {
this.setState({ title: event.target.value });
}
changeDescription() {
this.setState({ description: event.target.value });
}
changeColor() {
this.setState({ color: event.target.value });
}
submitColor() {
const { props, state } = this;
let color = state.color;
if (color.startsWith('#')) {
color = state.color.substr(1);
}
const hexExp = /([0-9A-Fa-f]{6})/;
const hexTest = hexExp.exec(color);
let currentColor = '000000';
if (props.association && 'metadata' in props.association) {
currentColor = uxToHex(props.association.metadata.color);
}
if (hexTest && (hexTest[1] !== currentColor)) {
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const association =
(props.association) && ('metadata' in props.association)
? props.association : {};
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
color
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}
}
deleteChat() {
const { props } = this;
this.setState({
isLoading: true,
awaiting: true,
type: (deSig(props.match.params.ship) === window.ship)
? 'Deleting chat...'
: 'Leaving chat...'
}, (() => {
props.api.chat.delete(props.station);
}));
}
groupifyChat() {
const { props, state } = this;
this.setState({
isLoading: true,
awaiting: true,
type: 'Converting chat...'
}, (() => {
props.api.chat.groupify(
props.station, state.targetGroup, state.inclusive
).then(() => this.setState({ awaiting: false }));
}));
}
renderDelete() {
const { props } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const deleteButtonClasses = (chatOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--gray3 gray3 bg-gray0-d c-default';
const leaveButtonClasses = (!chatOwner) ? 'pointer' : 'c-default';
return (
<div>
<div className={'w-100 fl mt3 ' + ((chatOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Leave Chat</p>
<p className="f9 gray2 db mb4">Remove this chat from your chat list. You will need to request for access again.</p>
<a onClick={(!chatOwner) ? this.deleteChat.bind(this) : null}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' + leaveButtonClasses}
>Leave this chat</a>
</div>
<div className={'w-100 fl mt3 ' + ((!chatOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Delete Chat</p>
<p className="f9 gray2 db mb4">Permanently delete this chat. All current members will no longer see this chat.</p>
<a onClick={(chatOwner) ? this.deleteChat.bind(this) : null}
className={'dib f9 ba pa2 ' + deleteButtonClasses}
>Delete this chat</a>
</div>
</div>
);
}
renderGroupify() {
const { props, state } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const groupPath = props.association['group-path'];
const ownedUnmanagedVillage =
chatOwner &&
!props.contacts[groupPath];
if (!ownedUnmanagedVillage) {
return null;
} else {
let inclusiveToggle = <div />;
if (state.targetGroup) {
inclusiveToggle = (
<div className="mt4">
<Toggle
boolean={state.inclusive}
change={this.changeInclusive}
/>
<span className="dib f9 white-d inter ml3">
Add all members to group
</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Add chat members to the group if they aren't in it yet
</p>
</div>
);
}
return (
<div>
<div className={'w-100 fl mt3'} style={{ maxWidth: '29rem' }}>
<p className="f8 mt3 lh-copy db">Convert Chat</p>
<p className="f9 gray2 db mb4">
Convert this chat into a group with associated chat, or select a
group to add this chat to.
</p>
<InviteSearch
groups={props.groups}
contacts={props.contacts}
associations={props.associations}
groupResults={true}
shipResults={false}
invites={{
groups: state.targetGroup ? [state.targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup}
/>
{inclusiveToggle}
<a onClick={this.groupifyChat.bind(this)}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black b--gray1-d pointer'}
>
Convert to group
</a>
</div>
</div>
);
}
}
renderMetadataSettings() {
const { props, state } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const association = (props.association) && ('metadata' in props.association)
? props.association : {};
return(
<div>
<div className={'w-100 pb6 fl mt3 ' + ((chatOwner) ? '' : 'o-30')}>
<p className="f8 mt3 lh-copy">Rename</p>
<p className="f9 gray2 db mb4">Change the name of this chat</p>
<div className="relative w-100 flex"
style={{ maxWidth: '29rem' }}
>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.title}
disabled={!chatOwner}
onChange={this.changeTitle}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}}
/>
</div>
<p className="f8 mt3 lh-copy">Change description</p>
<p className="f9 gray2 db mb4">Change the description of this chat</p>
<div className="relative w-100 flex"
style={{ maxWidth: '29rem' }}
>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.description}
disabled={!chatOwner}
onChange={this.changeDescription}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}}
/>
</div>
<p className="f8 mt3 lh-copy">Change color</p>
<p className="f9 gray2 db mb4">Give this chat a color when viewing group channels</p>
<div className="relative w-100 flex"
style={{ maxWidth: '10rem' }}
>
<div className="absolute"
style={{
height: 16,
width: 16,
backgroundColor: state.color,
top: 13,
left: 11
}}
/>
<input
className={'pl7 f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.color}
disabled={!chatOwner}
onChange={this.changeColor}
onBlur={this.submitColor}
/>
</div>
</div>
</div>
);
}
render() {
const { props, state } = this;
const isinPopout = this.props.popout ? 'popout/' : '';
const permission = Array.from(props.group.members.values());
if (state.isLoading) {
let title = props.station.substr(1);
if ((props.association) && ('metadata' in props.association)) {
title = (props.association.metadata.title !== '')
? props.association.metadata.title : props.station.substr(1);
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray2-d bg-gray0-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2 white-d"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={permission.length}
host={props.match.params.ship}
api={props.api}
/>
</div>
<div className="w-100 pl3 mt4 cf">
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
</div>
</div>
);
}
let title = props.station.substr(1);
if ((props.association) && ('metadata' in props.association)) {
title = (props.association.metadata.title !== '')
? props.association.metadata.title : props.station.substr(1);
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray1-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
api={this.props.api}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={permission.length}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={this.props.popout}
/>
</div>
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">Chat Settings</h2>
{this.renderGroupify()}
{this.renderDelete()}
{this.renderMetadataSettings()}
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
</div>
</div>
);
}
}

View File

@ -1,7 +1,6 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// import "./fonts/font.css";
import App from './App';
import App from './views/App';
ReactDOM.render(<App />, document.getElementById('root'));

View File

@ -1,6 +1,6 @@
import _ from "lodash";
import { uuid } from "../lib/util";
import { Patp, Path } from "../types/noun";
import { Patp, Path } from "~/types/noun";
import BaseStore from '../store/base';
export default class BaseApi<S extends object = {}> {

View File

@ -1,7 +1,7 @@
import BaseApi from './base';
import { uuid } from '../lib/util';
import { Letter, ChatAction, Envelope } from '../types/chat-update';
import { Patp, Path, PatpNoSig } from '../types/noun';
import { Letter, ChatAction, Envelope } from '~/types/chat-update';
import { Patp, Path, PatpNoSig } from '~/types/noun';
import { StoreState } from '../store/type';
import BaseStore from '../store/base';

View File

@ -1,8 +1,8 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Patp, Path, Enc } from '../types/noun';
import { Contact, ContactEdit } from '../types/contact-update';
import { GroupPolicy, Resource } from '../types/group-update';
import { Patp, Path, Enc } from '~/types/noun';
import { Contact, ContactEdit } from '~/types/contact-update';
import { GroupPolicy, Resource } from '~/types/group-update';
export default class ContactsApi extends BaseApi<StoreState> {
create(

View File

@ -1,4 +1,4 @@
import { Patp } from '../types/noun';
import { Patp } from '~/types/noun';
import BaseApi from './base';
import ChatApi from './chat';
import { StoreState } from '../store/type';

View File

@ -1,13 +1,13 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp, Enc } from '../types/noun';
import { Path, Patp, Enc } from '~/types/noun';
import {
GroupAction,
GroupPolicy,
Resource,
Tag,
GroupPolicyDiff,
} from '../types/group-update';
} from '~/types/group-update';
export default class GroupsApi extends BaseApi<StoreState> {
remove(resource: Resource, ships: Patp[]) {

View File

@ -1,6 +1,6 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { Serial, Path } from "../types/noun";
import { Serial, Path } from "~/types/noun";
export default class InviteApi extends BaseApi<StoreState> {
accept(app: Path, uid: Serial) {

View File

@ -2,7 +2,7 @@ import { stringToTa } from '../lib/util';
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path } from '../types/noun';
import { Path } from '~/types/noun';
export default class LinksApi extends BaseApi<StoreState> {

View File

@ -1,7 +1,7 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp } from '../types/noun';
import { Path, Patp } from '~/types/noun';
export default class MetadataApi extends BaseApi<StoreState> {

View File

@ -1,7 +1,7 @@
import BaseApi from './base';
import { PublishResponse } from '../types/publish-response';
import { PatpNoSig } from '../types/noun';
import { BookId, NoteId } from '../types/publish-update';
import { PublishResponse } from '~/types/publish-response';
import { PatpNoSig } from '~/types/noun';
import { BookId, NoteId } from '~/types/publish-update';
export default class PublishApi extends BaseApi {
handleEvent(data: PublishResponse) {

View File

@ -1,6 +1,6 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import {S3Update} from '../types/s3-update';
import {S3Update} from '../../types/s3-update';
export default class S3Api extends BaseApi<StoreState> {

View File

@ -1,5 +1,5 @@
import { roleTags, RoleTags, Group, Resource } from '../types/group-update';
import { PatpNoSig, Path } from '../types/noun';
import { roleTags, RoleTags, Group, Resource } from '~/types/group-update';
import { PatpNoSig, Path } from '~/types/noun';
export function roleForShip(group: Group, ship: PatpNoSig): RoleTags | undefined {

View File

@ -1,47 +1,76 @@
import defaultApps from './default-apps';
export default function index(associations, apps) {
const index = new Map([
const indexes = new Map([
['commands', []],
['subscriptions', []],
['groups', []],
['apps', []]
]);
// result schematic
const result = function(title, link, app, host) {
return {
'title': title,
'link': link,
'app': app,
'host': host
};
// result schematic
const result = function(title, link, app, host) {
return {
'title': title,
'link': link,
'app': app,
'host': host
};
};
const commandIndex = function () {
// commands are special cased for default suite
const commands = [];
defaultApps.filter((e) => {
return (e !== 'dojo');
}).map((e) => {
let title = e;
if (e === 'link') {
title = 'Links';
}
defaultApps
.filter((e) => {
return e !== 'dojo';
})
.map((e) => {
let title = e;
if (e === 'link') {
title = 'Links';
}
title = title.charAt(0).toUpperCase() + title.slice(1);
title = title.charAt(0).toUpperCase() + title.slice(1);
let obj = result(`${title}: Create`, `/~${e}/new`, e, null);
commands.push(obj);
if (title === 'Groups') {
obj = result(`${title}: Join Group`, `/~${e}/join`, title, null);
let obj = result(`${title}: Create`, `/~${e}/new`, e, null);
commands.push(obj);
}
});
if (title === 'Groups') {
obj = result(`${title}: Join Group`, `/~${e}/join`, title, null);
commands.push(obj);
}
});
commands.push(result('Profile', '/~profile', 'profile', null));
index.set('commands', commands);
return commands;
};
const appIndex = function (apps) {
// all apps are indexed from launch data
// indexed into 'apps'
const applications = [];
Object.keys(apps)
.filter((e) => {
return apps[e]?.type?.basic;
})
.map((e) => {
const obj = result(
apps[e].type.basic.title,
apps[e].type.basic.linkedUrl,
apps[e].type.basic.title,
null
);
applications.push(obj);
});
// add groups separately
applications.push(
result('Groups', '/~groups', 'groups', null)
);
return applications;
};
export default function index(associations, apps) {
// all metadata from all apps is indexed
// into subscriptions and groups
const subscriptions = [];
@ -74,15 +103,9 @@ export default function index(associations, apps) {
);
groups.push(obj);
} else {
let endpoint = '';
if (app === 'chat') {
endpoint = '/room';
} else if (app === 'publish') {
endpoint = '/notebook';
}
const obj = result(
title,
`/~${each['app-name']}${endpoint}${each['app-path']}`,
`/~${each['app-name']}/join${each['app-path']}`,
app.charAt(0).toUpperCase() + app.slice(1),
shipStart.slice(0, shipStart.indexOf('/'))
);
@ -90,19 +113,11 @@ export default function index(associations, apps) {
}
});
});
index.set('subscriptions', subscriptions);
index.set('groups', groups);
// all apps are indexed from launch data
// indexed into 'apps'
const applications = [];
Object.keys(apps).filter((e) => {
return (apps[e]?.type?.basic);
}).map((e) => {
const obj = result(apps[e].type.basic.title, apps[e].type.basic.linkedUrl, apps[e].type.basic.title, null);
applications.push(obj);
});
index.set('apps', applications);
indexes.set('commands', commandIndex());
indexes.set('subscriptions', subscriptions);
indexes.set('groups', groups);
indexes.set('apps', appIndex(apps));
return index;
return indexes;
};

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo, useEffect, useRef } from "react";
import { S3State } from "../types/s3-update";
import { S3State } from "../../types/s3-update";
import S3 from "aws-sdk/clients/s3";
export function useS3(s3: S3State) {

View File

@ -1,8 +1,8 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { ChatUpdate } from '../types/chat-update';
import { ChatHookUpdate } from '../types/chat-hook-update';
import { StoreState } from '../../../store/type';
import { Cage } from '~/types/cage';
import { ChatUpdate } from '~/types/chat-update';
import { ChatHookUpdate } from '~/types/chat-hook-update';
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
type LocalState = Pick<StoreState, 'connection'>;

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { ContactUpdate } from '../types/contact-update';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { ContactUpdate } from '~/types/contact-update';
type ContactState = Pick<StoreState, 'contacts'>;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import {
GroupUpdate,
Group,
@ -11,8 +11,8 @@ import {
OpenPolicy,
InvitePolicyDiff,
InvitePolicy,
} from '../types/group-update';
import { Enc, PatpNoSig } from '../types/noun';
} from '~/types/group-update';
import { Enc, PatpNoSig } from '~/types/noun';
import { resourceAsPath } from '../lib/util';
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { InviteUpdate } from '../types/invite-update';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { InviteUpdate } from '~/types/invite-update';
type InviteState = Pick<StoreState, "invites">;

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { LaunchUpdate } from '../types/launch-update';
import { Cage } from '../types/cage';
import { StoreState } from '../store/type';
import { LaunchUpdate } from '~/types/launch-update';
import { Cage } from '~/types/cage';
import { StoreState } from '../../store/type';
type LaunchState = Pick<StoreState, 'launch' | 'weather' | 'userLocation'>;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { LinkUpdate, Pagination } from '../types/link-update';
import { StoreState } from '../../store/type';
import { LinkUpdate, Pagination } from '~/types/link-update';
// page size as expected from link-view.
// must change in parallel with the +page-size in /app/link-view to

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { LinkListenUpdate } from '../types/link-listen-update';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { LinkListenUpdate } from '~/types/link-listen-update';
type LinkListenState = Pick<StoreState, 'linkListening'>;

View File

@ -1,7 +1,8 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { LocalUpdate, BackgroundConfig } from '../types/local-update';
import { StoreState } from '~/store/type';
import { Cage } from '~/types/cage';
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark'>;

View File

@ -1,9 +1,9 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { StoreState } from '../../store/type';
import { MetadataUpdate } from '../types/metadata-update';
import { Cage } from '../types/cage';
import { MetadataUpdate } from '~/types/metadata-update';
import { Cage } from '~/types/cage';
type MetadataState = Pick<StoreState, 'associations'>;

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { PermissionUpdate } from '../types/permission-update';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { PermissionUpdate } from '~/types/permission-update';
type PermissionState = Pick<StoreState, "permissions">;

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
type PublishState = Pick<StoreState, 'notebooks'>;

View File

@ -1,9 +1,9 @@
import _ from 'lodash';
import { PublishUpdate } from '../types/publish-update';
import { Cage } from '../types/cage';
import { StoreState } from '../store/type';
import { getTagFromFrond } from '../types/noun';
import { PublishUpdate } from '~/types/publish-update';
import { Cage } from '~/types/cage';
import { StoreState } from '../../store/type';
import { getTagFromFrond } from '~/types/noun';
type PublishState = Pick<StoreState, 'notebooks'>;

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { S3Update } from '../types/s3-update';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { S3Update } from '~/types/s3-update';
type S3State = Pick<StoreState, 's3'>;

View File

@ -5,7 +5,7 @@ import LocalReducer from '../reducers/local';
import ChatReducer from '../reducers/chat-update';
import { StoreState } from './type';
import { Cage } from '../types/cage';
import { Cage } from '~/types/cage';
import ContactReducer from '../reducers/contact-update';
import LinkUpdateReducer from '../reducers/link-update';
import S3Reducer from '../reducers/s3-update';

View File

@ -1,17 +1,17 @@
import { Inbox, Envelope } from '../types/chat-update';
import { ChatHookUpdate } from '../types/chat-hook-update';
import { Path } from '../types/noun';
import { Invites } from '../types/invite-update';
import { BackgroundConfig } from '../types/local-update';
import { Associations } from '../types/metadata-update';
import { Rolodex } from '../types/contact-update';
import { Notebooks } from '../types/publish-update';
import { Groups } from '../types/group-update';
import { S3State } from '../types/s3-update';
import { Permissions } from '../types/permission-update';
import { LaunchState, WeatherState } from '../types/launch-update';
import { LinkComments, LinkCollections, LinkSeen } from '../types/link-update';
import { ConnectionStatus } from '../types/connection';
import { Inbox, Envelope } from '~/types/chat-update';
import { ChatHookUpdate } from '~/types/chat-hook-update';
import { Path } from '~/types/noun';
import { Invites } from '~/types/invite-update';
import { Associations } from '~/types/metadata-update';
import { Rolodex } from '~/types/contact-update';
import { Notebooks } from '~/types/publish-update';
import { Groups } from '~/types/group-update';
import { S3State } from '~/types/s3-update';
import { Permissions } from '~/types/permission-update';
import { LaunchState, WeatherState } from '~/types/launch-update';
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
import { ConnectionStatus } from '~/types/connection';
import { BackgroundConfig } from '~/types/local-update';
export interface StoreState {
// local state

View File

@ -1,6 +1,6 @@
import BaseStore from "../store/base";
import BaseApi from "../api/base";
import { Path } from "../types/noun";
import { Path } from "~/types/noun";
export default class BaseSubscription<S extends object> {
private errorCount = 0;

View File

@ -1,6 +1,6 @@
import BaseSubscription from './base';
import { StoreState } from '../store/type';
import { Path } from '../types/noun';
import { Path } from '~/types/noun';
import _ from 'lodash';

View File

@ -13,34 +13,16 @@ import './css/fonts.css';
import light from './themes/light';
import dark from './themes/old-dark';
import LaunchApp from './apps/launch/app';
import ChatApp from './apps/chat/app';
import DojoApp from './apps/dojo/app';
import GroupsApp from './apps/groups/app';
import LinksApp from './apps/links/app';
import PublishApp from './apps/publish/app';
import Profile from './apps/profile/profile';
import { Content } from './components/Content';
import StatusBar from './components/StatusBar';
import Omnibox from './components/Omnibox';
import ErrorComponent from './components/Error';
import GlobalStore from './store/store';
import GlobalSubscription from './subscription/global';
import GlobalApi from './api/global';
import { uxToHex } from './lib/util';
import { Sigil } from './lib/sigil';
// const Style = createGlobalStyle`
// ${cssReset}
// html {
// background-color: ${p => p.theme.colors.white};
// }
//
// strong {
// font-weight: 600;
// }
// `;
import GlobalStore from '~/logic/store/store';
import GlobalSubscription from '~/logic/subscription/global';
import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
const Root = styled.div`
font-family: ${p => p.theme.fonts.sans};
@ -57,10 +39,6 @@ const Root = styled.div`
}
`;
const Content = styled.div`
height: calc(100% - 45px);
`;
const StatusBarWithRouter = withRouter(StatusBar);
class App extends React.Component {
@ -127,10 +105,9 @@ class App extends React.Component {
}
render() {
const channel = window.channel;
const associations = this.state.associations ? this.state.associations : { contacts: {} };
const { state } = this;
const associations = state.associations ?
state.associations : { contacts: {} };
const theme = state.dark ? dark : light;
const { background } = state;
@ -154,92 +131,11 @@ class App extends React.Component {
dark={state.dark}
show={state.omniboxShown}
/>
<Content>
<Switch>
<Route
exact
path='/'
render={p => (
<LaunchApp
ship={this.ship}
api={this.api}
{...state}
{...p}
/>
)}
/>
<Route
path='/~chat'
render={p => (
<ChatApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
<Route
path='/~dojo'
render={p => (
<DojoApp
ship={this.ship}
channel={channel}
subscription={this.subscription}
{...p}
/>
)}
/>
<Route
path='/~groups'
render={p => (
<GroupsApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
<Route
path='/~link'
render={p => (
<LinksApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
<Route
path='/~publish'
render={p => (
<PublishApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
<Route
path="/~profile"
render={ p => (
<Profile ship={this.ship} api={this.api} {...state} />
)}
/>
<Route
render={(props) => (
<ErrorComponent {...props} code={404} description="Not Found" />
)}
/>
</Switch>
</Content>
<Content
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state} />
</Router>
</Root>
</ThemeProvider>

View File

@ -11,11 +11,11 @@ import { SettingsScreen } from './components/settings';
import { NewScreen } from './components/new';
import { JoinScreen } from './components/join';
import { NewDmScreen } from './components/new-dm';
import { PatpNoSig } from '../../types/noun';
import GlobalApi from '../../api/global';
import { StoreState } from '../../store/type';
import GlobalSubscription from '../../subscription/global';
import {groupBunts} from '../../types/group-update';
import { PatpNoSig } from '~/types/noun';
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import GlobalSubscription from '~/logic/subscription/global';
import {groupBunts} from '~/types/group-update';
type ChatAppProps = StoreState & {
ship: PatpNoSig;
@ -281,44 +281,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
);
}}
/>
<Route
exact
path="/~chat/(popout)?/members/(~)?/:ship/:station+"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const popout = props.match.url.includes('/popout/');
const association =
station in associations['chat'] ? associations.chat[station] : {};
const groupPath = association['group-path'];
const group = groups[groupPath] || {};
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={sidebarShown}
popout={popout}
sidebar={renderChannelSidebar(props, station)}
>
<MemberScreen
{...props}
api={api}
group={group}
groups={groups}
associations={associations}
station={station}
association={association}
contacts={contacts}
popout={popout}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"

View File

@ -6,15 +6,15 @@ import { Link, RouteComponentProps } from "react-router-dom";
import { ChatWindow } from './lib/chat-window';
import { ChatHeader } from './lib/chat-header';
import { ChatInput } from "./lib/chat-input";
import { deSig } from "../../../lib/util";
import { ChatHookUpdate } from "../../../types/chat-hook-update";
import ChatApi from "../../../api/chat";
import { Inbox, Envelope } from "../../../types/chat-update";
import { Contacts } from "../../../types/contact-update";
import { Path, Patp } from "../../../types/noun";
import GlobalApi from "../../../api/global";
import { Association } from "../../../types/metadata-update";
import {Group} from "../../../types/group-update";
import { deSig } from "~/logic/lib/util";
import { ChatHookUpdate } from "~/types/chat-hook-update";
import ChatApi from "~/logic/api/chat";
import { Inbox, Envelope } from "~/types/chat-update";
import { Contacts } from "~/types/contact-update";
import { Path, Patp } from "~/types/noun";
import GlobalApi from "~/logic/api/global";
import { Association } from "~/types/metadata-update";
import {Group} from "~/types/group-update";
type ChatScreenProps = RouteComponentProps<{
@ -96,7 +96,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
props.chatSynced &&
!(props.station in props.chatSynced) &&
props.envelopes.length > 0;
const unreadCount = props.length - props.read;
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
@ -114,6 +114,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
sidebarShown={props.sidebarShown}
popout={props.popout} />
<ChatWindow
history={props.history}
isChatMissing={isChatMissing}
isChatLoading={isChatLoading}
isChatUnsynced={isChatUnsynced}
@ -142,6 +143,9 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
s3={props.s3}
placeholder="Message..."
message={this.state.messages.get(props.station) || ""}
deleteMessage={() => this.setState({
messages: this.state.messages.set(props.station, "")
})}
/>
</div>
);

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Spinner } from '../../../components/Spinner';
import { Spinner } from '~/views/components/Spinner';
import urbitOb from 'urbit-ob';
export class JoinScreen extends Component {

View File

@ -8,7 +8,7 @@ import 'codemirror/addon/display/placeholder';
import 'codemirror/lib/codemirror.css';
const BROWSER_REGEX =
new RegExp(String(!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i));
new RegExp(String(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i));
const MARKDOWN_CONFIG = {
@ -47,6 +47,7 @@ export default class ChatEditor extends Component {
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.message !== props.message) {
this.editor.setValue(props.message);
this.editor.setOption('mode', MARKDOWN_CONFIG);
@ -79,11 +80,17 @@ export default class ChatEditor extends Component {
return;
}
this.setState({ message: '' });
this.props.submit(editorMessage);
this.editor.setValue('');
}
messageChange(editor, data, value) {
if (this.state.message !== '' && value == '') {
this.setState({
message: value
});
}
if (value == this.props.message || value == '' || value == ' ') {
return;
}
@ -123,7 +130,7 @@ export default class ChatEditor extends Component {
onChange={(e, d, v) => this.messageChange(e, d, v)}
editorDidMount={(editor) => {
this.editor = editor;
if (BROWSER_REGEX.test(navigator.userAgent)) {
if (!(BROWSER_REGEX.test(navigator.userAgent))) {
editor.focus();
}
}}

View File

@ -0,0 +1,58 @@
import React, { Component, Fragment } from "react";
import { Link } from "react-router-dom";
import { ChatTabBar } from "./chat-tabbar";
import { SidebarSwitcher } from "~/views/components/SidebarSwitch";
import { deSig } from "~/logic/lib/util";
export const ChatHeader = (props) => {
const isInPopout = props.popout ? "popout/" : "";
const group = Array.from(props.group.members);
let title = props.station.substr(1);
if (props.association &&
"metadata" in props.association &&
props.association.metadata.tile !== "") {
title = props.association.metadata.title
}
return (
<Fragment>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
to={"/~chat/" + isInPopout + "room" + props.station}
className="pt2 white-d">
<h2
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}>
{title}
</h2>
</Link>
<ChatTabBar
location={props.location}
station={props.station}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={props.popout}
/>
</div>
</Fragment>
);
}

View File

@ -1,8 +1,9 @@
import React, { Component } from 'react';
import { Sigil } from '../../../../lib/sigil';
import ChatEditor from './chat-editor';
import { S3Upload } from './s3-upload';
import { uxToHex } from '../../../../lib/util';
import { S3Upload } from './s3-upload'
;
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
@ -10,7 +11,7 @@ const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\
export class ChatInput extends Component {
constructor(props) {
super(props);
this.state = {
inCodeMode: false,
};
@ -94,8 +95,10 @@ export class ChatInput extends Component {
let message = [];
let isInCodeBlock = false;
let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line) => {
message.push('\n');
text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) {
message.push('\n');
}
// A line of backticks enters and exits a codeblock
if (line.startsWith('```')) {
// But we need to check if we've ended a codeblock
@ -141,6 +144,8 @@ export class ChatInput extends Component {
messages.push(message);
}
props.deleteMessage();
messages.forEach((message) => {
if (message.length > 0) {
message = this.getLetterType(message.join(' '));
@ -171,27 +176,6 @@ export class ChatInput extends Component {
};
this.closure = closure.bind(this);
setTimeout(this.closure, 2000);*/
this.editor.setValue('');
}
toggleCode() {
if(this.state.code) {
this.setState({ code: false });
this.editor.setOption('mode', MARKDOWN_CONFIG);
this.editor.setOption('placeholder', this.props.placeholder);
} else {
this.setState({ code: true });
this.editor.setOption('mode', null);
this.editor.setOption('placeholder', 'Code...');
}
const value = this.editor.getValue();
// Force redraw of placeholder
if(value.length === 0) {
this.editor.setValue(' ');
this.editor.setValue('');
}
}
uploadSuccess(url) {

View File

@ -1,6 +1,6 @@
import React, { Component, Fragment } from "react";
import { scrollIsAtTop, scrollIsAtBottom } from "../../../../lib/util";
import { scrollIsAtTop, scrollIsAtBottom } from "~/logic/lib/util";
// Restore chat position on FF when new messages come in
const recalculateScrollTop = (lastScrollHeight, scrollContainer) => {

View File

@ -0,0 +1,41 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export const ChatTabBar = (props) => {
const {
location,
station
} = props;
let setColor = '', popout = '';
if (location.pathname.includes('/settings')) {
setColor = 'black white-d';
} else {
setColor = 'gray3';
}
const hidePopoutIcon = (popout)
? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl';
return (
<div className="dib flex-shrink-0 flex-grow-1">
<div className={'dib pt2 f9 pl6 pr6 lh-solid'}>
<Link
className={'no-underline ' + setColor}
to={'/~chat/' + popout + 'settings' + station}>
Settings
</Link>
</div>
<a href={'/~chat/popout/room' + station} rel="noopener noreferrer"
target="_blank"
className="dib fr pr1"
style={{ paddingTop: '8px' }}>
<img
className={'flex-shrink-0 pr3 dn ' + hidePopoutIcon}
src="/~chat/img/popout.png"
height="16"
width="16" />
</a>
</div>
);
}

View File

@ -99,6 +99,7 @@ export class ChatWindow extends Component {
scrollIsAtBottom() {
if (this.state.numPages !== 1) {
this.setState({ numPages: 1 });
this.dismissUnread();
}
}
@ -171,9 +172,12 @@ export class ChatWindow extends Component {
/>
{ messages.map((msg, i) => (
<ChatMessage
key={msg.uid}
unreadRef={this.unreadReference}
isLastUnread={
props.unreadCount > 0 && i === props.unreadCount - 1
props.unreadCount > 0 &&
i === props.unreadCount - 1 &&
state.numPages !== 1
}
msg={msg}
previousMsg={messages[i - 1]}

View File

@ -1,8 +1,7 @@
import React, { Component } from 'react';
import { Button } from '@tlon/indigo-react';
const IMAGE_REGEX =
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/;
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
const YOUTUBE_REGEX =
new RegExp(
@ -25,8 +24,7 @@ export default class UrlContent extends Component {
let unfoldState = this.state.unfold;
unfoldState = !unfoldState;
this.setState({ unfold: unfoldState });
const iframe = this.refs.iframe;
iframe.setAttribute('src', iframe.getAttribute('data-src'));
this.iframe.setAttribute('src', this.iframe.dataset.src);
}
render() {
@ -42,13 +40,12 @@ export default class UrlContent extends Component {
className="o-80-d"
src={content.url}
style={{
width: '50%',
maxWidth: '250px'
maxWidth: '18rem'
}}
></img>
);
return (
<a className={`f7 lh-copy v-top word-break-all`}
<a className='f7 lh-copy v-top word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"
@ -63,7 +60,9 @@ export default class UrlContent extends Component {
? 'db' : 'dn')}
>
<iframe
ref="iframe"
ref={(el) => {
this.iframe = el;
}}
width="560"
height="315"
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
@ -75,20 +74,26 @@ export default class UrlContent extends Component {
return (
<div>
<a href={content.url}
className={`f7 lh-copy v-top bb b--white-d word-break-all`}
href={content.url}
className='f7 lh-copy v-top bb b--white-d word-break-all'
target="_blank"
rel="noopener noreferrer">{content.url}</a>
<a className="ml2 f7 pointer lh-copy v-top"
onClick={e => this.unfoldEmbed()}>
[embed]
rel="noopener noreferrer"
>
{content.url}
</a>
<Button
border={1}
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
ml={1}
onClick={e => this.unfoldEmbed()}
>
{this.state.unfold ? 'collapse' : 'embed'}
</Button>
{contents}
</div>
);
} else {
return (
<a className={`f7 lh-copy v-top bb b--white-d b--black word-break-all`}
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"

View File

@ -0,0 +1,51 @@
import React, { Component } from 'react';
export const DeleteButton = (props) => {
const { isOwner, station, changeLoading, api } = props;
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
const deleteButtonClasses = (isOwner) ?
'b--red2 red2 pointer bg-gray0-d' :
'b--gray3 gray3 bg-gray0-d c-default';
const deleteChat = () => {
changeLoading(
true,
true,
isOwner ? 'Deleting chat...' : 'Leaving chat...',
() => {
api.chat.delete(station);
}
);
};
return (
<div className="w-100 cf">
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Leave Chat</p>
<p className="f9 gray2 db mb4">
Remove this chat from your chat list.{' '}
You will need to request for access again.
</p>
<a onClick={(!isOwner) ? deleteChat : null}
className={
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
leaveButtonClasses
}>
Leave this chat
</a>
</div>
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Delete Chat</p>
<p className="f9 gray2 db mb4">
Permanently delete this chat.{' '}
All current members will no longer see this chat.
</p>
<a onClick={(isOwner) ? deleteChat : null}
className={'dib f9 ba pa2 ' + deleteButtonClasses}
>Delete this chat</a>
</div>
</div>
);
};

View File

@ -0,0 +1,104 @@
import React, { Component } from 'react';
import Toggle from '~/views/components/toggle';
import { InviteSearch } from '~/views/components/InviteSearch';
export class GroupifyButton extends Component {
constructor(props) {
super(props);
this.state = {
inclusive: false,
targetGroup: null
};
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
renderInclusiveToggle() {
return this.state.targetGroup ? (
<div className="mt4">
<Toggle
boolean={inclusive}
change={this.changeInclusive.bind(this)}
/>
<span className="dib f9 white-d inter ml3">
Add all members to group
</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Add chat members to the group if they aren't in it yet
</p>
</div>
) : <div />;
}
render() {
const { inclusive, targetGroup } = this.state;
const {
api,
isOwner,
association,
associations,
contacts,
groups,
station
} = this.props;
const groupPath = association['group-path'];
const ownedUnmanagedVillage =
isOwner &&
!contacts[groupPath];
if (!ownedUnmanagedVillage) {
return null;
}
return (
<div className={'w-100 fl mt3'} style={{ maxWidth: '29rem' }}>
<p className="f8 mt3 lh-copy db">Convert Chat</p>
<p className="f9 gray2 db mb4">
Convert this chat into a group with associated chat, or select a
group to add this chat to.
</p>
<InviteSearch
groups={groups}
contacts={contacts}
associations={associations}
groupResults={true}
shipResults={false}
invites={{
groups: targetGroup ? [targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup.bind(this)}
/>
{this.renderInclusiveToggle()}
<a onClick={() => {
changeLoading(true, true, 'Converting to group...', () => {
api.chat.groupify(
station, targetGroup, inclusive
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}}
className={
'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black ' +
'b--gray1-d pointer'
}>Convert to group</a>
</div>
);
}
}

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { InviteSearch } from '../../../../components/InviteSearch';
import { Spinner } from '../../../../components/Spinner';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Spinner } from '~/views/components/Spinner';
export class InviteElement extends Component {
constructor(props) {

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { OverlaySigil } from './overlay-sigil';
import MessageContent from './message-content';
import { uxToHex, cite, writeText } from '../../../../lib/util';
import { uxToHex, cite, writeText } from '~/logic/lib/util';
import moment from 'moment';

View File

@ -0,0 +1,71 @@
import React, { Component } from 'react';
import { uxToHex } from '~/logic/lib/util';
export class MetadataColor extends Component {
constructor(props) {
super(props);
this.state = {
color: props.initialValue
};
this.changeColor = this.changeColor.bind(this);
this.submitColor = this.submitColor.bind(this);
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.initialValue !== props.initialValue) {
this.setState({ color: props.initialValue });
}
}
changeColor(event) {
this.setState({ color: event.target.value });
}
submitColor() {
const { props, state } = this;
let color = state.color;
if (color.startsWith('#')) {
color = state.color.substr(1);
}
const hexExp = /([0-9A-Fa-f]{6})/;
const hexTest = hexExp.exec(color);
if (!props.isDisabled && hexTest && (state.color !== props.initialValue)) {
props.setValue(color);
}
}
render() {
const { props, state } = this;
return (
<div className={'cf w-100 mb3 ' + ((props.isDisabled) ? 'o-30' : '')}>
<p className="f8 lh-copy">Change color</p>
<p className="f9 gray2 db mb4">Give this chat a color when viewing group channels</p>
<div className="relative w-100 flex"
style={{ maxWidth: '10rem' }}
>
<div className="absolute"
style={{
height: 16,
width: 16,
backgroundColor: state.color,
top: 13,
left: 11
}} />
<input
className={'pl7 f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.color}
disabled={props.isDisabled}
onChange={this.changeColor}
onBlur={this.submitColor} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
export class MetadataInput extends Component {
constructor(props) {
super(props);
this.state = {
value: props.initialValue
};
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.initialValue !== props.initialValue) {
this.setState({ value: props.initialValue });
}
}
render() {
const {
title,
description,
isDisabled,
setValue
} = this.props;
return (
<div className={'w-100 mb3 fl ' + ((isDisabled) ? 'o-30' : '')}>
<p className="f8 lh-copy">{title}</p>
<p className="f9 gray2 db mb4">{description}</p>
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
type="text"
value={this.state.value}
disabled={isDisabled}
onChange={(e) => {
this.setState({ value: e.target.value });
}}
onBlur={() => {
if (!isDisabled) {
setValue(this.state.value || '');
}
}}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { MetadataColor } from './metadata-color';
import { MetadataInput } from './metadata-input';
import { uxToHex } from '~/logic/lib/util';
export const MetadataSettings = (props) => {
const {
isOwner,
association,
changeLoading,
api,
station
} = props;
const title =
(props.association && 'metadata' in props.association) ?
association.metadata.title : '';
const description =
(props.association && 'metadata' in props.association) ?
association.metadata.description : '';
const color =
(props.association && 'metadata' in props.association) ?
`#${uxToHex(props.association.metadata.color)}` : '';
return (
<div className="cf mt6">
<MetadataInput
title='Rename'
description='Change the name of this chat'
isDisabled={!isOwner}
initialValue={title}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
val,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
<MetadataInput
title='Change description'
description='Change the description of this chat'
isDisabled={!isOwner}
initialValue={description}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
val,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
<MetadataColor
initialValue={color}
isDisabled={!isOwner}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
val
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
</div>
);
};

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Sigil } from '../../../../lib/sigil';
import { Sigil } from '~/logic/lib/sigil';
import {
ProfileOverlay,
OVERLAY_HEIGHT

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { cite } from '../../../../lib/util';
import { Sigil } from '../../../../lib/sigil';
import { cite } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
export const OVERLAY_HEIGHT = 250;

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import S3Client from '../../../../lib/s3';
import S3Client from '~/logic/lib/s3';
export class S3Upload extends Component {
constructor(props) {

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import { Spinner } from '../../../components/Spinner';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { InviteSearch } from '../../../components/InviteSearch';
import { InviteSearch } from '~/views/components/InviteSearch';
import urbitOb from 'urbit-ob';
import { deSig } from '../../../lib/util';
import { deSig } from '~/logic/lib/util';
export class NewDmScreen extends Component {
constructor(props) {

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react';
import { InviteSearch } from '../../../components/InviteSearch';
import { Spinner } from '../../../components/Spinner';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { deSig } from '../../../lib/util';
import { deSig } from '~/logic/lib/util';
export class NewScreen extends Component {
constructor(props) {

View File

@ -0,0 +1,145 @@
import React, { Component, Fragment } from 'react';
import { deSig } from '~/logic/lib/util';
import { Link } from 'react-router-dom';
import { ChatHeader } from './lib/chat-header';
import { MetadataSettings } from './lib/metadata-settings';
import { DeleteButton } from './lib/delete-button';
import { GroupifyButton } from './lib/groupify-button';
import { Spinner } from '~/views/components/Spinner';
import { ChatTabBar } from './lib/chat-tabbar';
import SidebarSwitcher from '~/views/components/SidebarSwitch';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
awaiting: false,
type: 'Editing chat...'
};
this.changeLoading = this.changeLoading.bind(this);
}
componentDidMount() {
if (this.state.isLoading && (this.props.station in this.props.inbox)) {
this.setState({ isLoading: false });
}
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (state.isLoading && !(props.station in props.inbox)) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~chat');
});
} else if (state.isLoading && (props.station in props.inbox)) {
this.setState({ isLoading: false });
}
}
changeLoading(isLoading, awaiting, type, closure) {
this.setState({
isLoading,
awaiting,
type
}, closure);
}
renderLoading() {
return (
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
);
}
renderNormal() {
const { state } = this;
const {
associations,
association,
contacts,
groups,
api,
station,
match
} = this.props;
const isOwner = deSig(match.params.ship) === window.ship;
return (
<Fragment>
<h2 className="f8 pb2">Chat Settings</h2>
<GroupifyButton
isOwner={isOwner}
association={association}
associations={associations}
contacts={contacts}
groups={groups}
api={api}
changeLoading={this.changeLoading} />
<DeleteButton
isOwner={isOwner}
changeLoading={this.changeLoading}
station={station}
api={api} />
<MetadataSettings
isOwner={isOwner}
changeLoading={this.changeLoading}
api={api}
association={association}
station={station} />
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
</Fragment>
);
}
render() {
const { state } = this;
const {
api,
group,
association,
station,
popout,
sidebarShown,
match,
location
} = this.props;
const isInPopout = popout ? "popout/" : "";
const title =
( association &&
('metadata' in association) &&
(association.metadata.title !== '')
) ? association.metadata.title : station.substr(1);
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<ChatHeader
match={match}
location={location}
api={api}
group={group}
association={association}
station={station}
sidebarShown={sidebarShown}
popout={popout} />
<div className="w-100 pl3 mt4 cf">
{(state.isLoading) ? this.renderLoading() : this.renderNormal() }
</div>
</div>
);
}
}

View File

@ -1,8 +1,8 @@
import React, { Component } from 'react';
import Welcome from './lib/welcome';
import { alphabetiseAssociations } from '../../../lib/util';
import SidebarInvite from '../../../components/SidebarInvite';
import { alphabetiseAssociations } from '~/logic/lib/util';
import SidebarInvite from '~/views/components/SidebarInvite';
import { GroupItem } from './lib/group-item';
export class Sidebar extends Component {

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import ErrorBoundary from '../../../components/ErrorBoundary';
import ErrorBoundary from '~/views/components/ErrorBoundary';
export class Skeleton extends Component {
render() {

View File

@ -30,7 +30,7 @@ export default class DojoApp extends Component {
componentDidMount() {
document.title = 'OS1 - Dojo';
const channel = new this.props.channel();
const channel = new window.channel();
this.api = new Api(this.props.ship, channel);
this.store.api = this.api;

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { cite } from '../../../lib/util';
import { Spinner } from '../../../components/Spinner';
import { cite } from '~/logic/lib/util';
import { Spinner } from '~/views/components/Spinner';
export class Input extends Component {
constructor(props) {

Some files were not shown because too many files have changed in this diff Show More