mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-05 13:55:54 +03:00
chat-view: redesign of chat interface
This commit redesigns the front-end of chat-view for Landscape, adding a collapsable sidebar, popout chats, a streamlined join flow, and a general refresh of the Indigo interface.
This commit is contained in:
parent
e8d34fe0ca
commit
a6b4ed19b3
File diff suppressed because one or more lines are too long
BIN
pkg/arvo/app/chat/img/ChatSwitcherClosed.png
Normal file
BIN
pkg/arvo/app/chat/img/ChatSwitcherClosed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 866 B |
BIN
pkg/arvo/app/chat/img/ChatSwitcherLink.png
Normal file
BIN
pkg/arvo/app/chat/img/ChatSwitcherLink.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 861 B |
BIN
pkg/arvo/app/chat/img/popout.png
Normal file
BIN
pkg/arvo/app/chat/img/popout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 854 B |
@ -5,8 +5,20 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
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="stylesheet" href="/~chat/css/index.css" />
|
<link rel="stylesheet" href="/~chat/css/index.css" />
|
||||||
<link rel="icon" type="image/png" href="/~launch/img/Favicon.png">
|
<link rel="icon" type="image/png" href="/~launch/img/Favicon.png">
|
||||||
|
<link rel="manifest"
|
||||||
|
href='data:application/manifest+json,{
|
||||||
|
"name": "Chat",
|
||||||
|
"short_name": "Chat",
|
||||||
|
"description": "A%20Chat%20application%20for%20your%20Urbit%20ship.",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "%23FFFFFF",
|
||||||
|
"theme_color": "%23000000"}' />
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" />
|
<div id="root" />
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,3 +1,8 @@
|
|||||||
|
* {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
|
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
|
||||||
margin-block-end: unset;
|
margin-block-end: unset;
|
||||||
margin-block-start: unset;
|
margin-block-start: unset;
|
||||||
@ -14,25 +19,22 @@ textarea, input, button {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=checkbox] {
|
.dropdown::after {
|
||||||
-webkit-appearance: checkbox;
|
content: "⌃";
|
||||||
|
transform: rotate(180deg);
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #000 !important;
|
color: #000;
|
||||||
font-weight: 400 !important;
|
font-weight: 400;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 32px;
|
font-weight: 400;
|
||||||
line-height: 48px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-regular {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.body-large {
|
.body-large {
|
||||||
@ -53,67 +55,6 @@ h2 {
|
|||||||
|
|
||||||
.label-regular {
|
.label-regular {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-small {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-small-mono {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-family: "Source Code Pro", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-regular-400 {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plus-font {
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-font {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 16px;
|
|
||||||
font-weight: 600 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fw-normal {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fw-bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fs-italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.td-underline {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-v-light-gray {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nice-green {
|
|
||||||
color: #2AA779 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-nice-green {
|
|
||||||
background: #2ED196;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nice-red {
|
|
||||||
color: #EE5432 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inter {
|
.inter {
|
||||||
@ -146,6 +87,54 @@ h2 {
|
|||||||
font-family: "Source Code Pro", monospace;
|
font-family: "Source Code Pro", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-small-mono.list-ship {
|
.list-ship {
|
||||||
line-height: 29px;
|
line-height: 2.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-default {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* responsive */
|
||||||
|
|
||||||
|
@media all and (max-width: 34.375em) {
|
||||||
|
.dn-s {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.flex-basis-full-s {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
.h-100-minus-48-s {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
.h-100-minus-96-s {
|
||||||
|
height: calc(100% - 96px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
|
||||||
|
.flex-basis-300-m {
|
||||||
|
flex-basis: 300px;
|
||||||
|
}
|
||||||
|
.h-100-minus-48-m {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 46.875em) and (max-width: 60em) {
|
||||||
|
.flex-basis-300-l {
|
||||||
|
flex-basis: 300px;
|
||||||
|
}
|
||||||
|
.h-100-minus-48-l {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 60em) {
|
||||||
|
.flex-basis-300-xl {
|
||||||
|
flex-basis: 300px;
|
||||||
|
}
|
||||||
|
.h-100-minus-48-xl {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
}
|
1
pkg/interface/chat/src/css/indigo-static.css
Normal file
1
pkg/interface/chat/src/css/indigo-static.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
@import "css/tachyons.css";
|
@import "css/indigo-static.css";
|
||||||
@import "css/fonts.css";
|
@import "css/fonts.css";
|
||||||
@import "css/spinner.css";
|
@import "css/spinner.css";
|
||||||
@import "css/custom.css";
|
@import "css/custom.css";
|
||||||
|
@ -164,7 +164,7 @@ class UrbitApi {
|
|||||||
ship: `~${window.ship}`,
|
ship: `~${window.ship}`,
|
||||||
recipient: ship,
|
recipient: ship,
|
||||||
app: 'chat-hook',
|
app: 'chat-hook',
|
||||||
text: `You have been invited to /${window.ship}${path}`,
|
text: `~${window.ship}${path}`,
|
||||||
},
|
},
|
||||||
uid: uuid()
|
uid: uuid()
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ import React, { Component } from 'react';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
import { store } from "/store";
|
||||||
|
|
||||||
|
|
||||||
import { Message } from '/components/lib/message';
|
import { Message } from '/components/lib/message';
|
||||||
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
||||||
import { ChatInput } from '/components/lib/chat-input';
|
import { ChatInput } from '/components/lib/chat-input';
|
||||||
@ -9,213 +13,271 @@ import { deSig } from '/lib/util';
|
|||||||
|
|
||||||
|
|
||||||
export class ChatScreen extends Component {
|
export class ChatScreen extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
station: `/${props.match.params.ship}/${props.match.params.station}`,
|
station: `/${props.match.params.ship}/${props.match.params.station}`,
|
||||||
numPages: 1,
|
numPages: 1,
|
||||||
scrollLocked: false,
|
scrollLocked: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.hasAskedForMessages = false;
|
this.hasAskedForMessages = false;
|
||||||
this.onScroll = this.onScroll.bind(this);
|
this.onScroll = this.onScroll.bind(this);
|
||||||
|
|
||||||
this.updateReadInterval = setInterval(
|
this.updateReadInterval = setInterval(
|
||||||
this.updateReadNumber.bind(this),
|
this.updateReadNumber.bind(this),
|
||||||
1000
|
1000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateReadNumber();
|
this.updateReadNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.updateReadInterval) {
|
if (this.updateReadInterval) {
|
||||||
clearInterval(this.updateReadInterval);
|
clearInterval(this.updateReadInterval);
|
||||||
this.updateReadInterval = null;
|
this.updateReadInterval = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
if ((prevProps.match.params.station !== props.match.params.station) ||
|
if (
|
||||||
(prevProps.match.params.ship !== props.match.params.ship)) {
|
prevProps.match.params.station !== props.match.params.station ||
|
||||||
this.hasAskedForMessages = false;
|
prevProps.match.params.ship !== props.match.params.ship
|
||||||
|
) {
|
||||||
|
this.hasAskedForMessages = false;
|
||||||
|
|
||||||
clearInterval(this.updateReadInterval);
|
clearInterval(this.updateReadInterval);
|
||||||
|
|
||||||
this.setState({
|
this.setState(
|
||||||
station: `/${props.match.params.ship}/${props.match.params.station}`,
|
{
|
||||||
scrollLocked: false
|
station: `/${props.match.params.ship}/${props.match.params.station}`,
|
||||||
}, () => {
|
scrollLocked: false
|
||||||
this.scrollToBottom();
|
},
|
||||||
this.updateReadInterval = setInterval(
|
() => {
|
||||||
this.updateReadNumber.bind(this),
|
this.scrollToBottom();
|
||||||
1000
|
this.updateReadInterval = setInterval(
|
||||||
);
|
this.updateReadNumber.bind(this),
|
||||||
this.updateReadNumber();
|
1000
|
||||||
});
|
);
|
||||||
} else if (Object.keys(props.inbox).length === 0) {
|
this.updateReadNumber();
|
||||||
props.history.push('/~chat');
|
}
|
||||||
} else if (props.envelopes.length - prevProps.envelopes.length >= 200) {
|
);
|
||||||
this.hasAskedForMessages = false;
|
} else if (Object.keys(props.inbox).length === 0) {
|
||||||
}
|
props.history.push("/~chat");
|
||||||
}
|
} else if (
|
||||||
|
props.envelopes.length - prevProps.envelopes.length >=
|
||||||
|
200
|
||||||
|
) {
|
||||||
|
this.hasAskedForMessages = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateReadNumber() {
|
updateReadNumber() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
if (props.read < props.length) {
|
if (props.read < props.envelopes.length) {
|
||||||
props.api.chat.read(state.station);
|
props.api.chat.read(state.station);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
askForMessages() {
|
askForMessages() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
if (state.numPages * 100 < props.envelopes.length - 400 ||
|
|
||||||
this.hasAskedForMessages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.envelopes.length > 0) {
|
if (
|
||||||
let end = props.envelopes[0].number;
|
state.numPages * 100 < props.envelopes.length - 400 ||
|
||||||
if (end > 0) {
|
this.hasAskedForMessages
|
||||||
let start = ((end - 400) > 0) ? end - 400 : 0;
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (start === 0 && end === 1) {
|
if (props.envelopes.length > 0) {
|
||||||
return;
|
let end = props.envelopes[0].number;
|
||||||
}
|
if (end > 0) {
|
||||||
|
let start = end - 400 > 0 ? end - 400 : 0;
|
||||||
|
|
||||||
this.hasAskedForMessages = true;
|
if (start === 0 && end === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
props.subscription.fetchMessages(start, end - 1, state.station);
|
this.hasAskedForMessages = true;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
props.subscription.fetchMessages(start, end - 1, state.station);
|
||||||
if (!this.state.scrollLocked && this.scrollElement) {
|
}
|
||||||
this.scrollElement.scrollIntoView({ behavior: 'smooth' });
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onScroll(e) {
|
scrollToBottom() {
|
||||||
if (navigator.userAgent.includes('Safari') &&
|
if (!this.state.scrollLocked && this.scrollElement) {
|
||||||
navigator.userAgent.includes('Chrome')) {
|
this.scrollElement.scrollIntoView({ behavior: "smooth" });
|
||||||
// Google Chrome
|
}
|
||||||
if (e.target.scrollTop === 0) {
|
}
|
||||||
this.setState({
|
|
||||||
numPages: this.state.numPages + 1,
|
|
||||||
scrollLocked: true
|
|
||||||
}, () => {
|
|
||||||
this.askForMessages();
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
(e.target.scrollHeight - Math.round(e.target.scrollTop)) ===
|
|
||||||
(e.target.clientHeight)
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
numPages: 1,
|
|
||||||
scrollLocked: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (navigator.userAgent.includes('Safari')) {
|
|
||||||
// Safari
|
|
||||||
if (e.target.scrollTop === 0) {
|
|
||||||
this.setState({
|
|
||||||
numPages: 1,
|
|
||||||
scrollLocked: false
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
(e.target.scrollHeight + Math.round(e.target.scrollTop)) <=
|
|
||||||
(e.target.clientHeight + 10)
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
numPages: this.state.numPages + 1,
|
|
||||||
scrollLocked: true
|
|
||||||
}, () => {
|
|
||||||
this.askForMessages();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Your browser is not supported.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
onScroll(e) {
|
||||||
const { props, state } = this;
|
if (
|
||||||
|
navigator.userAgent.includes("Safari") &&
|
||||||
|
navigator.userAgent.includes("Chrome")
|
||||||
|
) {
|
||||||
|
// Google Chrome
|
||||||
|
if (e.target.scrollTop === 0) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
numPages: this.state.numPages + 1,
|
||||||
|
scrollLocked: true
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.askForMessages();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
e.target.scrollHeight - Math.round(e.target.scrollTop) ===
|
||||||
|
e.target.clientHeight
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
numPages: 1,
|
||||||
|
scrollLocked: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (navigator.userAgent.includes("Safari")) {
|
||||||
|
// Safari
|
||||||
|
if (e.target.scrollTop === 0) {
|
||||||
|
this.setState({
|
||||||
|
numPages: 1,
|
||||||
|
scrollLocked: false
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
e.target.scrollHeight + Math.round(e.target.scrollTop) <=
|
||||||
|
e.target.clientHeight + 10
|
||||||
|
) {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
numPages: this.state.numPages + 1,
|
||||||
|
scrollLocked: true
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.askForMessages();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Your browser is not supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let messages = props.envelopes.slice(0);
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
let lastMsgNum = (messages.length > 0) ?
|
|
||||||
messages.length : 0;
|
|
||||||
|
|
||||||
if (messages.length > 100 * state.numPages) {
|
let messages = props.envelopes.slice(0);
|
||||||
messages = messages
|
|
||||||
.slice(messages.length - (100 * state.numPages), messages.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
let pendingMessages =
|
let lastMsgNum = messages.length > 0 ? messages.length : 0;
|
||||||
props.pendingMessages.has(state.station)
|
|
||||||
? props.pendingMessages.get(state.station) : [];
|
|
||||||
|
|
||||||
pendingMessages.map(function(value) {
|
|
||||||
return value.pending = true;
|
|
||||||
})
|
|
||||||
|
|
||||||
let reversedMessages = messages.concat(pendingMessages);
|
|
||||||
reversedMessages = reversedMessages.reverse();
|
|
||||||
|
|
||||||
reversedMessages = reversedMessages.map((msg, i) => {
|
if (messages.length > 100 * state.numPages) {
|
||||||
// Render sigil if previous message is not by the same sender
|
messages = messages.slice(
|
||||||
let aut = ['author'];
|
messages.length - 100 * state.numPages,
|
||||||
let renderSigil =
|
messages.length
|
||||||
_.get(reversedMessages[i + 1], aut) !== _.get(msg, aut, msg.author);
|
);
|
||||||
let paddingTop = renderSigil;
|
}
|
||||||
let paddingBot =
|
|
||||||
_.get(reversedMessages[i - 1], aut) !== _.get(msg, aut, msg.author);
|
|
||||||
|
|
||||||
return (
|
let pendingMessages = props.pendingMessages.has(state.station)
|
||||||
<Message
|
? props.pendingMessages.get(state.station)
|
||||||
key={msg.uid}
|
: [];
|
||||||
msg={msg}
|
|
||||||
renderSigil={renderSigil}
|
|
||||||
paddingTop={paddingTop}
|
|
||||||
paddingBot={paddingBot}
|
|
||||||
pending={!!msg.pending} />
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let group = Array.from(props.group.values());
|
pendingMessages.map(function(value) {
|
||||||
|
return (value.pending = true);
|
||||||
return (
|
});
|
||||||
<div key={state.station}
|
|
||||||
className="h-100 w-100 overflow-hidden flex flex-column">
|
let reversedMessages = messages.concat(pendingMessages);
|
||||||
<div className='pl3 pt2 bb'>
|
reversedMessages = reversedMessages.reverse();
|
||||||
<h2>{state.station.substr(1)}</h2>
|
|
||||||
<ChatTabBar {...props}
|
reversedMessages = reversedMessages.map((msg, i) => {
|
||||||
station={state.station}
|
// Render sigil if previous message is not by the same sender
|
||||||
numPeers={group.length}
|
let aut = ["author"];
|
||||||
isOwner={deSig(props.match.params.ship) === window.ship} />
|
let renderSigil =
|
||||||
</div>
|
_.get(reversedMessages[i + 1], aut) !==
|
||||||
<div
|
_.get(msg, aut, msg.author);
|
||||||
className="overflow-y-scroll pt3 pb2 flex flex-column-reverse"
|
let paddingTop = renderSigil;
|
||||||
style={{ height: 'calc(100% - 157px)', resize: 'vertical' }}
|
let paddingBot =
|
||||||
onScroll={this.onScroll}>
|
_.get(reversedMessages[i - 1], aut) !==
|
||||||
<div ref={ el => { this.scrollElement = el; }}></div>
|
_.get(msg, aut, msg.author);
|
||||||
{reversedMessages}
|
|
||||||
</div>
|
return (
|
||||||
<ChatInput
|
<Message
|
||||||
api={props.api}
|
key={msg.uid}
|
||||||
numMsgs={lastMsgNum}
|
msg={msg}
|
||||||
station={state.station}
|
renderSigil={renderSigil}
|
||||||
owner={deSig(props.match.params.ship)}
|
paddingTop={paddingTop}
|
||||||
permissions={props.permissions}
|
paddingBot={paddingBot}
|
||||||
placeholder='Message...' />
|
pending={!!msg.pending}
|
||||||
</div>
|
/>
|
||||||
)
|
);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
let group = Array.from(props.group.values());
|
||||||
|
|
||||||
|
let popoutSwitcher = this.props.popout ? "dn-m dn-l dn-xl" : "dib-m dib-l dib-xl";
|
||||||
|
let isinPopout = this.props.popout ? "popout/" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={state.station}
|
||||||
|
className="h-100 w-100 overflow-hidden flex flex-column">
|
||||||
|
<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="pl3 pt4 bb b--gray4 flex relative overflow-x-scroll flex-shrink-0"
|
||||||
|
style={{ height: 48 }}>
|
||||||
|
<a className="pointer flex-shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
store.setState(previousState => ({
|
||||||
|
sidebarShown: !previousState.sidebarShown
|
||||||
|
}));
|
||||||
|
}}>
|
||||||
|
<img className={`v-btm pr3 dn ` + popoutSwitcher}
|
||||||
|
src={
|
||||||
|
this.props.sidebarShown
|
||||||
|
? "/~chat/img/ChatSwitcherLink.png"
|
||||||
|
: "/~chat/img/ChatSwitcherClosed.png"
|
||||||
|
}
|
||||||
|
height="16"
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<Link to={`/~chat/` + isinPopout + `room` + state.station}>
|
||||||
|
<h2
|
||||||
|
className="mono dib f7 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}>
|
||||||
|
{state.station.substr(1)}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<ChatTabBar
|
||||||
|
{...props}
|
||||||
|
station={state.station}
|
||||||
|
numPeers={group.length}
|
||||||
|
isOwner={deSig(props.match.params.ship) === window.ship}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-scroll pt3 pb2 flex flex-column-reverse"
|
||||||
|
style={{ height: "100%", resize: "vertical" }}
|
||||||
|
onScroll={this.onScroll}>
|
||||||
|
<div ref={el => {
|
||||||
|
this.scrollElement = el;
|
||||||
|
}}></div>
|
||||||
|
{reversedMessages}
|
||||||
|
</div>
|
||||||
|
<ChatInput
|
||||||
|
api={props.api}
|
||||||
|
numMsgs={lastMsgNum}
|
||||||
|
station={state.station}
|
||||||
|
owner={deSig(props.match.params.ship)}
|
||||||
|
permissions={props.permissions}
|
||||||
|
placeholder="Message..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { Route, Link } from 'react-router-dom';
|
||||||
import urbitOb from 'urbit-ob';
|
import urbitOb from 'urbit-ob';
|
||||||
|
|
||||||
|
|
||||||
@ -16,6 +17,24 @@ export class JoinScreen extends Component {
|
|||||||
this.stationChange = this.stationChange.bind(this);
|
this.stationChange = this.stationChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.autoJoin !== "undefined/undefined") {
|
||||||
|
let station = this.props.autoJoin.split('/');
|
||||||
|
let ship = station[0];
|
||||||
|
station.splice(0, 1);
|
||||||
|
station = '/' + station.join('/');
|
||||||
|
|
||||||
|
if (station.length < 2 || !urbitOb.isValidPatp(ship)) {
|
||||||
|
this.setState({
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.api.chatView.join(ship, station);
|
||||||
|
this.props.history.push('/~chat');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
if (state.station in props.inbox) {
|
if (state.station in props.inbox) {
|
||||||
@ -61,15 +80,15 @@ export class JoinScreen extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
|
|
||||||
let joinClasses = "db label-regular mt4 btn-font pointer underline bn";
|
let joinClasses = "db f9 green2 ba pa2 b--green2";
|
||||||
if (!this.state.station) {
|
if ((!this.state.station) || (this.state.station === "/")) {
|
||||||
joinClasses = joinClasses + ' gray';
|
joinClasses = 'db f9 gray2 ba pa2 b--gray3';
|
||||||
}
|
}
|
||||||
|
|
||||||
let errElem = (<span />);
|
let errElem = (<span />);
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
errElem = (
|
errElem = (
|
||||||
<span className="body-small inter nice-red db">
|
<span className="f9 inter red2 db">
|
||||||
Chat must have a valid name.
|
Chat must have a valid name.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -77,16 +96,17 @@ export class JoinScreen extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 pa3 pt2 overflow-x-hidden flex flex-column">
|
<div className="h-100 w-100 pa3 pt2 overflow-x-hidden flex flex-column">
|
||||||
<h2 className="mb3">Join</h2>
|
<div
|
||||||
<div className="w-50">
|
className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||||
<p className="body-medium mt3 db">Chatroom</p>
|
<Link to="/~chat/">{"⟵ All Chats"}</Link>
|
||||||
<p className="body-small db mt2 mb3">
|
</div>
|
||||||
Join an existing chatroom.
|
<h2 className="mb3 f8">Join Existing Chat</h2>
|
||||||
Chatrooms follow the format ~shipname/chat-name.
|
<div className="w-100">
|
||||||
</p>
|
<p className="f8 lh-copy mt3 db">Enter a <span className="mono">~ship/chat-name</span></p>
|
||||||
|
<p className="f9 gray2 mb4">Chat names use lowercase, hyphens, and slashes.</p>
|
||||||
<textarea
|
<textarea
|
||||||
ref={ e => { this.textarea = e; } }
|
ref={ e => { this.textarea = e; } }
|
||||||
className="body-regular mono fw-normal ba pa2 mb2 db w-100"
|
className="f7 mono ba b--gray3 pa3 mb2 db"
|
||||||
placeholder="~zod/chatroom"
|
placeholder="~zod/chatroom"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
rows={1}
|
rows={1}
|
||||||
@ -99,8 +119,7 @@ export class JoinScreen extends Component {
|
|||||||
<button
|
<button
|
||||||
onClick={this.onClickJoin.bind(this)}
|
onClick={this.onClickJoin.bind(this)}
|
||||||
className={joinClasses}
|
className={joinClasses}
|
||||||
style={{ fontSize: '18px' }}
|
>Join Chat</button>
|
||||||
>-> Join</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,6 @@ import Mousetrap from 'mousetrap';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
import { IconSend } from '/components/lib/icons/icon-send';
|
|
||||||
|
|
||||||
import { uuid } from '/lib/util';
|
import { uuid } from '/lib/util';
|
||||||
|
|
||||||
@ -155,16 +154,16 @@ export class ChatInput extends Component {
|
|||||||
|
|
||||||
readOnlyRender() {
|
readOnlyRender() {
|
||||||
return (
|
return (
|
||||||
<div className="mt2 pa3 cf flex black bt o-50">
|
<div className="pa3 cf flex black bt b--gray4 o-50">
|
||||||
<div className="fl" style={{
|
<div className="fl" style={{
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
flexBasis: 32,
|
flexBasis: 24,
|
||||||
height: 36
|
height: 24
|
||||||
}}>
|
}}>
|
||||||
<Sigil ship={window.ship} size={32} />
|
<Sigil ship={window.ship} size={24} color="#4330FC" />
|
||||||
</div>
|
</div>
|
||||||
<div className="fr h-100 flex pa2" style={{ flexGrow: 1, height: 40 }}>
|
<div className="fr h-100 flex" style={{ flexGrow: 1, height: 28, paddingTop: 6, resize: "none" }}>
|
||||||
<p style={{paddingTop: 3}}>This chat is read only and you cannot post.</p>
|
<p className="pl3">This chat is read only and you cannot post.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -176,27 +175,26 @@ export class ChatInput extends Component {
|
|||||||
this.bindShortcuts();
|
this.bindShortcuts();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pa3 cf flex black bt b--black-30" style={{ flexGrow: 1 }}>
|
<div className="pa3 cf flex black bt b--gray4" style={{ flexGrow: 1 }}>
|
||||||
<div className="fl" style={{
|
<div
|
||||||
marginTop: 4,
|
className="fl"
|
||||||
flexBasis: 32,
|
style={{
|
||||||
height: 36
|
marginTop: 4,
|
||||||
}}>
|
flexBasis: 24,
|
||||||
<Sigil ship={window.ship} size={32} />
|
height: 24
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sigil ship={window.ship} size={24} color="#4330FC" />
|
||||||
</div>
|
</div>
|
||||||
<div className="fr h-100 flex" style={{ flexGrow: 1 }}>
|
<div className="fr h-100 flex" style={{ flexGrow: 1 }}>
|
||||||
<textarea
|
<textarea
|
||||||
className={'ml2 mt2 mr2 bn'}
|
className={"pl3 bn"}
|
||||||
style={{ flexGrow: 1, height: 40, paddingTop: 3, resize: 'none' }}
|
style={{ flexGrow: 1, height: 28, paddingTop: 6, resize: "none" }}
|
||||||
ref={this.textareaRef}
|
ref={this.textareaRef}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
value={state.message}
|
value={state.message}
|
||||||
onChange={this.messageChange}
|
onChange={this.messageChange}
|
||||||
autoFocus={true}
|
|
||||||
/>
|
/>
|
||||||
<div className="pointer" onClick={this.messageSubmit}>
|
|
||||||
<IconSend />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -8,54 +8,59 @@ export class ChatTabBar extends Component {
|
|||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
|
|
||||||
let bbStream = '',
|
let memColor = '',
|
||||||
bbMembers = '',
|
setColor = '',
|
||||||
bbSettings = '';
|
popout = '';
|
||||||
|
|
||||||
let strColor = '',
|
|
||||||
memColor = '',
|
|
||||||
setColor = '';
|
|
||||||
|
|
||||||
if (props.location.pathname.includes('/settings')) {
|
if (props.location.pathname.includes('/settings')) {
|
||||||
bbSettings = ' bb';
|
memColor = 'gray3';
|
||||||
strColor = 'gray';
|
|
||||||
memColor = 'gray';
|
|
||||||
setColor = 'black';
|
setColor = 'black';
|
||||||
} else if (props.location.pathname.includes('/members')) {
|
} else if (props.location.pathname.includes('/members')) {
|
||||||
bbMembers = ' bb';
|
|
||||||
strColor = 'gray';
|
|
||||||
memColor = 'black';
|
memColor = 'black';
|
||||||
setColor = 'gray';
|
setColor = 'gray3';
|
||||||
} else {
|
} else {
|
||||||
bbStream = ' bb';
|
memColor = 'gray3';
|
||||||
strColor = 'black';
|
setColor = 'gray3';
|
||||||
memColor = 'gray';
|
|
||||||
setColor = 'gray';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let membersText = props.numPeers === 1
|
(props.location.pathname.includes('/popout'))
|
||||||
? '1 Member' : `${props.numPeers} Members`;
|
? popout = "popout/"
|
||||||
|
: popout = "";
|
||||||
|
|
||||||
|
let hidePopoutIcon = (this.props.popout)
|
||||||
|
? "dn-m dn-l dn-xl"
|
||||||
|
: "dib-m dib-l dib-xl";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100" style={{ height:28 }}>
|
<div className="dib flex-shrink-0-m flex-grow-1">
|
||||||
<div className={"dib h-100" + bbStream} style={{width:'160px'}}>
|
{!!props.isOwner ? (
|
||||||
<Link
|
<div className={"dib f8 pl6"}>
|
||||||
className={'no-underline label-regular v-mid ' + strColor}
|
|
||||||
to={'/~chat/room' + props.station}>Stream</Link>
|
|
||||||
</div>
|
|
||||||
{ !!props.isOwner ? (
|
|
||||||
<div className={"dib h-100" + bbMembers} style={{width:'160px'}}>
|
|
||||||
<Link
|
<Link
|
||||||
className={'no-underline label-regular v-mid ' + memColor}
|
className={"no-underline v-top " + memColor}
|
||||||
to={'/~chat/members' + props.station}>{membersText}</Link>
|
to={`/~chat/` + popout + `members` + props.station}>
|
||||||
|
Members
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : <div className="dib" style={{width:0}}></div>
|
) : (
|
||||||
}
|
<div className="dib" style={{ width: 0 }}></div>
|
||||||
<div className={"dib h-100" + bbSettings} style={{width:'160px'}}>
|
)}
|
||||||
|
<div className={"dib f8 pl6 pr6"}>
|
||||||
<Link
|
<Link
|
||||||
className={'no-underline label-regular v-mid ' + setColor}
|
className={"no-underline v-top " + setColor}
|
||||||
to={'/~chat/settings' + props.station}>Settings</Link>
|
to={`/~chat/` + popout + `settings` + props.station}>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<a href={`/~chat/popout/room` + props.station} target="_blank"
|
||||||
|
className="dib fr">
|
||||||
|
<img
|
||||||
|
className={`v-btm flex-shrink-0 pr2 dn ` + hidePopoutIcon}
|
||||||
|
src="/~chat/img/popout.png"
|
||||||
|
height="16"
|
||||||
|
width="16"
|
||||||
|
style={{ paddingTop: "2px" }}/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,12 @@ export class HeaderBar extends Component {
|
|||||||
</div>
|
</div>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
let popoutHide = (this.props.popout)
|
||||||
|
? "dn dn-m dn-l dn-xl"
|
||||||
|
: "dn db-m db-l db-xl";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-black w-100 justify-between"
|
<div className={`bg-black w-100 justify-between ` + popoutHide}
|
||||||
style={{ height: 48, padding: 8}}>
|
style={{ height: 48, padding: 8}}>
|
||||||
<a className="db"
|
<a className="db"
|
||||||
style={{ background: '#1A1A1A',
|
style={{ background: '#1A1A1A',
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export class IconSend extends Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<img src="/~chat/img/Send.png" width={40} height={40} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,17 +13,13 @@ export class Sigil extends Component {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ flexBasis: 32, backgroundColor: props.color }}>
|
||||||
className="bg-black"
|
{sigil({
|
||||||
style={{ flexBasis: 32 }}>
|
|
||||||
{
|
|
||||||
sigil({
|
|
||||||
patp: props.ship,
|
patp: props.ship,
|
||||||
renderer: reactRenderer,
|
renderer: reactRenderer,
|
||||||
size: props.size,
|
size: props.size,
|
||||||
colors: ['black', 'white'],
|
colors: [props.color, "white"]
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -69,39 +69,40 @@ export class InviteElement extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { props, state} = this;
|
const { props, state} = this;
|
||||||
let errorElem = !!state.error ? (
|
let errorElem = !!state.error ? (
|
||||||
<p className="pt2 nice-red label-regular">Invalid ship name.</p>
|
<p className="pt2 red2 f8">Invalid ship name.</p>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let successElem = !!state.success ? (
|
let successElem = !!state.success ? (
|
||||||
<p className="pt2 nice-green label-regular">Success!</p>
|
<p className="pt2 green2 f8">Success!</p>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
let modifyButtonClasses = "label-regular black underline btn-font pointer";
|
let modifyButtonClasses = "db f9 ba pa2 b--black pointer";
|
||||||
if (!state.error) {
|
if (state.error) {
|
||||||
modifyButtonClasses = modifyButtonClasses + ' black';
|
modifyButtonClasses = modifyButtonClasses + ' gray3';
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonText = '';
|
let buttonText = '';
|
||||||
if (props.permissions.kind === 'black') {
|
if (props.permissions.kind === 'black') {
|
||||||
buttonText = '-> Ban';
|
buttonText = 'Ban';
|
||||||
} else if (props.permissions.kind === 'white') {
|
} else if (props.permissions.kind === 'white') {
|
||||||
buttonText = '-> Invite';
|
buttonText = 'Invite';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<textarea
|
||||||
ref={ e => { this.textarea = e; } }
|
ref={ e => { this.textarea = e; } }
|
||||||
className="w-90 db ba overflow-y-hidden mono gray mb2"
|
className="f7 mono ba b--gray3 pa3 mb4 db w-100"
|
||||||
style={{
|
style={{
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
height: 150
|
height: 50
|
||||||
}}
|
}}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
|
placeholder="~zod, ~bus"
|
||||||
onChange={this.modifyMembersChange.bind(this)}></textarea>
|
onChange={this.modifyMembersChange.bind(this)}></textarea>
|
||||||
<button
|
<button
|
||||||
onClick={this.modifyMembers.bind(this)}
|
onClick={this.modifyMembers.bind(this)}
|
||||||
|
@ -16,15 +16,15 @@ export class MemberElement extends Component {
|
|||||||
let actionElem;
|
let actionElem;
|
||||||
if (props.ship === props.owner) {
|
if (props.ship === props.owner) {
|
||||||
actionElem = (
|
actionElem = (
|
||||||
<p className="dib w-20 underline black label-small-mono label-regular">
|
<p className="w-20 dib list-ship black f8 c-default">
|
||||||
Host
|
Host
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else if (window.ship !== props.ship && window.ship === props.owner) {
|
} else if (window.ship !== props.ship && window.ship === props.owner) {
|
||||||
actionElem = (
|
actionElem = (
|
||||||
<a onClick={this.onRemove.bind(this)}
|
<a onClick={this.onRemove.bind(this)}
|
||||||
className="w-20 dib list-ship black underline label-small-mono pointer">
|
className="w-20 dib list-ship black f8 pointer">
|
||||||
Remove
|
Ban
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -38,9 +38,9 @@ export class MemberElement extends Component {
|
|||||||
<Sigil ship={props.ship} size={32} />
|
<Sigil ship={props.ship} size={32} />
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
"w-70 dib v-mid black ml2 nowrap label-small-mono list-ship label-regular"
|
"w-70 mono list-ship dib v-mid black ml2 nowrap f8"
|
||||||
}>
|
}>
|
||||||
{props.ship}
|
~{props.ship}
|
||||||
</p>
|
</p>
|
||||||
{actionElem}
|
{actionElem}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { Route, Link } from 'react-router-dom'
|
||||||
|
import urbitOb from 'urbit-ob';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
@ -45,7 +47,7 @@ export class Message extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a className="body-regular-400 v-top"
|
<a className="f7 lh-copy v-top bb b--black"
|
||||||
href={letter.url}
|
href={letter.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
@ -54,47 +56,73 @@ export class Message extends Component {
|
|||||||
);
|
);
|
||||||
} else if ('me' in letter) {
|
} else if ('me' in letter) {
|
||||||
return (
|
return (
|
||||||
<p className='body-regular-400 v-top'>
|
<p className='f7 lh-copy v-top'>
|
||||||
{letter.me}
|
{letter.me}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
let chatroom = letter.text.match(
|
||||||
<p className='body-regular-400 v-top'>
|
/(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/
|
||||||
{letter.text}
|
);
|
||||||
</p>
|
if ((chatroom !== null)
|
||||||
);
|
&& (chatroom[1].length > 2)
|
||||||
}
|
&& (urbitOb.isValidPatp(chatroom[1]))) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="bb b--black f7 mono lh-copy v-top"
|
||||||
|
to={"/~chat/join/" + chatroom.input}>
|
||||||
|
{letter.text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<p className='f7 lh-copy v-top'>
|
||||||
|
{letter.text}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
let pending = !!props.msg.pending ? ' o-40' : '';
|
let pending = !!props.msg.pending ? ' o-40' : '';
|
||||||
let datestamp = moment.unix(props.msg.when / 1000).format('LL');
|
let datestamp = "~" + moment.unix(props.msg.when / 1000).format('YYYY.MM.D');
|
||||||
|
|
||||||
let paddingTop = props.paddingTop ? 'pt3' : '';
|
let paddingTop = props.paddingTop ? {'paddingTop': '6px'} : '';
|
||||||
let paddingBot = props.paddingBot ? 'pb2' : 'pb1';
|
|
||||||
|
|
||||||
if (props.renderSigil) {
|
if (props.renderSigil) {
|
||||||
let timestamp = moment.unix(props.msg.when / 1000).format('hh:mm a');
|
let timestamp = moment.unix(props.msg.when / 1000).format('hh:mm a');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"w-100 pl3 pr3 cf flex " + paddingTop + " " + paddingBot + pending}
|
<div
|
||||||
style={{
|
className={
|
||||||
minHeight: 'min-content'
|
"w-100 f8 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
|
||||||
}}>
|
}
|
||||||
<div className="fl mr2">
|
style={{
|
||||||
<Sigil ship={props.msg.author} size={36} />
|
minHeight: "min-content"
|
||||||
|
}}>
|
||||||
|
<div className="fl mr3 v-top">
|
||||||
|
<Sigil
|
||||||
|
ship={props.msg.author}
|
||||||
|
size={24}
|
||||||
|
color={((props.msg.author === window.ship)
|
||||||
|
|| (props.msg.author.substr(1) === window.ship))
|
||||||
|
? "#4330FC"
|
||||||
|
: "#000000"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="fr clamp-message" style={{ flexGrow: 1, marginTop: -8 }}>
|
<div
|
||||||
<div className="hide-child">
|
className="fr clamp-message"
|
||||||
<p className="v-top label-small-mono gray dib mr3">
|
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||||
|
<div className="hide-child" style={paddingTop}>
|
||||||
|
<p className="v-mid mono f9 gray dib mr3">
|
||||||
|
{props.msg.author.slice(0, 1) === "~" ? "" : "~"}
|
||||||
{props.msg.author}
|
{props.msg.author}
|
||||||
</p>
|
</p>
|
||||||
<p className="v-top label-small-mono gray dib">{timestamp}</p>
|
<p className="v-mid mono f9 gray dib">{timestamp}</p>
|
||||||
<p className="v-top label-small-mono ml2 gray dib child">
|
<p className="v-mid mono f9 ml2 gray dib child">{datestamp}</p>
|
||||||
{datestamp}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</div>
|
</div>
|
||||||
@ -104,16 +132,17 @@ export class Message extends Component {
|
|||||||
let timestamp = moment.unix(props.msg.when / 1000).format('hh:mm');
|
let timestamp = moment.unix(props.msg.when / 1000).format('hh:mm');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"w-100 pr3 pb1 cf hide-child flex" + pending}
|
<div
|
||||||
style={{
|
className={"w-100 pr3 cf hide-child flex" + pending}
|
||||||
minHeight: 'min-content'
|
style={{
|
||||||
}}>
|
minHeight: "min-content"
|
||||||
<p className="child pl3 pr2 label-small-mono gray dib">{timestamp}</p>
|
}}>
|
||||||
<div className="fr clamp-message" style={{ flexGrow: 1 }}>
|
<p className="child pt2 pl2 pr1 mono f9 gray dib">{timestamp}</p>
|
||||||
|
<div className="fr f7 clamp-message" style={{ flexGrow: 1 }}>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,17 +18,12 @@ export class SidebarInvite extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className='pa3'>
|
<div className='pa3'>
|
||||||
<div className='w-100 v-mid'>
|
<div className='w-100 v-mid'>
|
||||||
<div className="dib mr2 bg-nice-green" style={{
|
<p className="dib f8 mono">
|
||||||
borderRadius: 12,
|
|
||||||
width: 12,
|
|
||||||
height: 12
|
|
||||||
}}></div>
|
|
||||||
<p className="dib body-regular fw-normal">
|
|
||||||
{props.invite.text}
|
{props.invite.text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a className="dib w-50 pointer btn-font nice-green underline" onClick={this.onAccept.bind(this)}>Accept</a>
|
<a className="dib pointer pa2 f9 bg-green2 white mt4" onClick={this.onAccept.bind(this)}>Accept Invite</a>
|
||||||
<a className="dib w-50 tr pointer btn-font nice-red underline" onClick={this.onDecline.bind(this)}>Decline</a>
|
<a className="dib pointer ml4 pa2 f9 bg-black white mt4" onClick={this.onDecline.bind(this)}>Decline</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -50,30 +50,37 @@ export class SidebarItem extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
let unreadElem = !!props.unread ? (
|
let unreadElem = !!props.unread
|
||||||
<div
|
? "fw7 green2"
|
||||||
className="bg-nice-green dib mr2"
|
: "";
|
||||||
style={{ borderRadius: 6, width: 12, height: 12 }}>
|
|
||||||
</div>
|
let title = props.title.substr(1);
|
||||||
) : (
|
|
||||||
<div className="dib"></div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let description = this.getLetter(props.description);
|
let description = this.getLetter(props.description);
|
||||||
|
|
||||||
let selectedCss = !!props.selected ? 'bg-light-gray' : 'bg-white pointer';
|
let selectedCss = !!props.selected ? 'bg-gray5' : 'bg-white pointer';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'pa3 ' + selectedCss} onClick={this.onClick.bind(this)}>
|
<div
|
||||||
<div className='w-100 v-mid'>
|
className={"z1 pa3 pt4 pb4 bb b--gray4 " + selectedCss}
|
||||||
{unreadElem}
|
onClick={this.onClick.bind(this)}>
|
||||||
<p className="dib body-regular lh-16">{props.title.substr(1)}</p>
|
<div className="w-100 v-mid">
|
||||||
|
<p className={"dib mono f8 " + unreadElem }>
|
||||||
|
<span className={(unreadElem === "") ? "gray3" : ""}>
|
||||||
|
{title.substr(0, title.indexOf("/"))}/
|
||||||
|
</span>
|
||||||
|
{title.substr(title.indexOf("/") + 1)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100">
|
<div className="w-100 pt1">
|
||||||
<p className='dib gray label-small-mono mr3 lh-16'>{props.ship}</p>
|
<p className="dib mono f9 mr3">
|
||||||
<p className='dib gray label-small-mono lh-16'>{state.timeSinceNewestMessage}</p>
|
{props.ship === "" ? "" : "~"}
|
||||||
|
{props.ship}
|
||||||
|
</p>
|
||||||
|
<p className="dib mono f9 gray3">{state.timeSinceNewestMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className='label-small gray clamp-3 lh-16 pt1'>{description}</p>
|
<p className="f8 clamp-3 pt2">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
import { store } from "/store";
|
||||||
|
|
||||||
import urbitOb from 'urbit-ob';
|
import urbitOb from 'urbit-ob';
|
||||||
import { deSig } from '/lib/util';
|
import { deSig } from '/lib/util';
|
||||||
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
||||||
@ -69,56 +72,86 @@ export class MemberScreen extends Component {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let popoutSwitcher = this.props.popout
|
||||||
|
? "dn-m dn-l dn-xl"
|
||||||
|
: "dib-m dib-l dib-xl";
|
||||||
|
let isinPopout = this.props.popout ? "popout/" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
||||||
<div className='pl3 pt2 bb mb3'>
|
<div className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||||
<h2>{state.station.substr(1)}</h2>
|
style={{ height: "1rem" }}>
|
||||||
|
<Link to="/~chat/">{"⟵ All Chats"}</Link>
|
||||||
|
</div>
|
||||||
|
<div className="pl3 pt4 bb b--gray4 flex relative overflow-x-scroll flex-shrink-0"
|
||||||
|
style={{ height: 48 }}>
|
||||||
|
<a className="pointer"
|
||||||
|
onClick={() => {
|
||||||
|
store.setState(previousState => ({
|
||||||
|
sidebarShown: !previousState.sidebarShown
|
||||||
|
}));
|
||||||
|
}}>
|
||||||
|
<img className={`v-btm pr3 dn ` + popoutSwitcher}
|
||||||
|
src={
|
||||||
|
this.props.sidebarShown
|
||||||
|
? "/~chat/img/ChatSwitcherLink.png"
|
||||||
|
: "/~chat/img/ChatSwitcherClosed.png"
|
||||||
|
}
|
||||||
|
height="16"
|
||||||
|
width="16"/>
|
||||||
|
</a>
|
||||||
|
<Link to={`/~chat/` + isinPopout + `room` + state.station}>
|
||||||
|
<h2
|
||||||
|
className="mono dib f7 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}>
|
||||||
|
{state.station.substr(1)}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
<ChatTabBar
|
<ChatTabBar
|
||||||
{...props}
|
{...props}
|
||||||
station={state.station}
|
station={state.station}
|
||||||
numPeers={writeGroup.length}
|
numPeers={writeGroup.length}
|
||||||
isOwner={deSig(props.match.params.ship) === window.ship} />
|
isOwner={deSig(props.match.params.ship) === window.ship}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 cf">
|
<div className="w-100 pl3 mt4 cf pr6">
|
||||||
<div className="w-50 fl pa2 pr3">
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
<p className="body-regular mb3">Members</p>
|
<p className="f8 pb2">Members</p>
|
||||||
<p className="label-regular gray mb3">{writeText}</p>
|
<p className="f9 gray2 mb3">{writeText}</p>
|
||||||
{writeListMembers}
|
{writeListMembers}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-50 fr pa2 pl3">
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
<p className="body-regular mb3">Modify Permissions</p>
|
<p className="f8 pb2">Modify Permissions</p>
|
||||||
<p className="label-regular gray mb3">
|
<p className="f9 gray2 mb3">{modWriteText}</p>
|
||||||
{modWriteText}
|
{window.ship === deSig(props.match.params.ship) ? (
|
||||||
</p>
|
|
||||||
{ window.ship === deSig(props.match.params.ship) ? (
|
|
||||||
<InviteElement
|
<InviteElement
|
||||||
path={`/chat${state.station}/write`}
|
path={`/chat${state.station}/write`}
|
||||||
station={`/${props.match.params.station}`}
|
station={`/${props.match.params.station}`}
|
||||||
permissions={props.write}
|
permissions={props.write}
|
||||||
api={props.api} />
|
api={props.api}
|
||||||
) : null }
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 cf mt2">
|
<div className="w-100 pl3 mt4 cf pr6">
|
||||||
<div className="w-50 fl pa2 pr3">
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
<p className="label-regular gray mb3">{readText}</p>
|
<p className="f9 gray2 db mb3">{readText}</p>
|
||||||
{readListMembers}
|
{readListMembers}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-50 fr pa2 pl3">
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
<p className="label-regular gray mb3">
|
<p className="f9 gray2 db mb3">{modReadText}</p>
|
||||||
{modReadText}
|
{window.ship === deSig(props.match.params.ship) ? (
|
||||||
</p>
|
<InviteElement
|
||||||
{ window.ship === deSig(props.match.params.ship) ?
|
path={`/chat${state.station}/read`}
|
||||||
( <InviteElement
|
station={`/${props.match.params.station}`}
|
||||||
path={`/chat${state.station}/read`}
|
permissions={props.read}
|
||||||
station={`/${props.match.params.station}`}
|
api={props.api}
|
||||||
permissions={props.read}
|
/>
|
||||||
api={props.api}/>
|
) : null}
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { uuid, isPatTa, deSig } from '/lib/util';
|
import { uuid, isPatTa, deSig } from '/lib/util';
|
||||||
import urbitOb from 'urbit-ob';
|
import urbitOb from 'urbit-ob';
|
||||||
|
|
||||||
|
|
||||||
export class NewScreen extends Component {
|
export class NewScreen extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -13,6 +13,7 @@ export class NewScreen extends Component {
|
|||||||
idName: '',
|
idName: '',
|
||||||
invites: '',
|
invites: '',
|
||||||
security: 'village',
|
security: 'village',
|
||||||
|
securityDescription: 'Invite-only chat. Default membership administration.',
|
||||||
idError: false,
|
idError: false,
|
||||||
inviteError: false,
|
inviteError: false,
|
||||||
allowHistory: true
|
allowHistory: true
|
||||||
@ -33,6 +34,27 @@ export class NewScreen extends Component {
|
|||||||
props.history.push('/~chat/room' + station);
|
props.history.push('/~chat/room' + station);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevState.security !== this.state.security) {
|
||||||
|
|
||||||
|
let securityText = '';
|
||||||
|
|
||||||
|
switch (this.state.security) {
|
||||||
|
case 'village':
|
||||||
|
securityText = 'Invite-only chat. Default membership administration.';
|
||||||
|
break;
|
||||||
|
case 'channel':
|
||||||
|
securityText = 'Completely public chat. Default membership administration.';
|
||||||
|
break;
|
||||||
|
case 'journal':
|
||||||
|
securityText = 'Similar to a blog. Publicly readable/subscribable, invited members can write to journal.'
|
||||||
|
break;
|
||||||
|
case 'mailbox':
|
||||||
|
securityText = 'Similar to email. Anyone can write to the mailbox, invited members can read messages.'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setState({ securityDescription: securityText });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
idChange(event) {
|
idChange(event) {
|
||||||
@ -138,15 +160,15 @@ export class NewScreen extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let createClasses = "db label-regular mt4 btn-font pointer underline bn";
|
let createClasses = "pointer db f9 green2 ba pa2 b--green2";
|
||||||
if (!this.state.idName) {
|
if (!this.state.idName) {
|
||||||
createClasses = createClasses + ' gray';
|
createClasses = 'pointer db f9 gray2 ba pa2 b--gray3';
|
||||||
}
|
}
|
||||||
|
|
||||||
let idErrElem = (<span />);
|
let idErrElem = (<span />);
|
||||||
if (this.state.idError) {
|
if (this.state.idError) {
|
||||||
idErrElem = (
|
idErrElem = (
|
||||||
<span className="body-small inter nice-red db">
|
<span className="f9 inter red2 db">
|
||||||
Chat must have a valid name.
|
Chat must have a valid name.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -155,22 +177,25 @@ export class NewScreen extends Component {
|
|||||||
let invErrElem = (<span />);
|
let invErrElem = (<span />);
|
||||||
if (this.state.inviteError) {
|
if (this.state.inviteError) {
|
||||||
invErrElem = (
|
invErrElem = (
|
||||||
<span className="body-small inter nice-red db">
|
<span className="f9 inter red2 db">
|
||||||
Invites must be validly formatted ship names.
|
Invites must be validly formatted ship names.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 pa3 pt2 overflow-x-hidden flex flex-column">
|
<div className="h-100 w-100 w-50-l w-50-xl pa3 pt2 overflow-x-hidden flex flex-column">
|
||||||
<h2 className="mb3">Create</h2>
|
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||||
<div className="w-50">
|
<Link to="/~chat/">{"⟵ All Chats"}</Link>
|
||||||
<p className="body-medium db">Chat Name</p>
|
</div>
|
||||||
<p className="body-small db mt2 mb3">
|
<h2 className="mb3 f8">Create New Chat</h2>
|
||||||
Name this chat. Names must be lowercase and only contain letters, numbers, and dashes.
|
<div className="w-100">
|
||||||
|
<p className="f8 mt3 lh-copy db">Chat Name</p>
|
||||||
|
<p className="f9 gray2 db mb4">
|
||||||
|
Alphanumeric characters, dashes, and slashes only
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="body-regular fw-normal ba pa2 db w-100"
|
className="f7 ba b--gray3 pa3 db w-100"
|
||||||
placeholder="secret-chat"
|
placeholder="secret-chat"
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{
|
style={{
|
||||||
@ -178,13 +203,28 @@ export class NewScreen extends Component {
|
|||||||
}}
|
}}
|
||||||
onChange={this.idChange} />
|
onChange={this.idChange} />
|
||||||
{idErrElem}
|
{idErrElem}
|
||||||
<p className="body-medium mt3 db">Invites</p>
|
<p className="f8 mt6 lh-copy db">Chat Type</p>
|
||||||
<p className="body-small db mt2 mb3">
|
<p className="f9 gray2 db mb4">Change the chat's visibility and type</p>
|
||||||
Invite new participants to this chat.
|
<div className="dropdown relative">
|
||||||
|
<select
|
||||||
|
style={{WebkitAppearance: "none"}}
|
||||||
|
className="pa3 f8 bg-white br0 w-100 inter"
|
||||||
|
value={this.state.securityValue}
|
||||||
|
onChange={this.securityChange}>
|
||||||
|
<option value="village">Village</option>
|
||||||
|
<option value="channel">Channel</option>
|
||||||
|
<option value="journal">Journal</option>
|
||||||
|
<option value="mailbox">Mailbox</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p className="f9 gray2 db lh-copy pt2 mb4">{this.state.securityDescription}</p>
|
||||||
|
<p className="f8 mt4 lh-copy db">Invites</p>
|
||||||
|
<p className="f9 gray2 db mb4">
|
||||||
|
Invite participants to this chat
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
ref={ e => { this.textarea = e; } }
|
ref={e => { this.textarea = e; }}
|
||||||
className="body-regular mono fw-normal ba pa2 mb2 db w-100"
|
className="f7 mono ba b--gray3 pa3 mb4 db w-100"
|
||||||
placeholder="~zod, ~bus"
|
placeholder="~zod, ~bus"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
style={{
|
style={{
|
||||||
@ -193,31 +233,10 @@ export class NewScreen extends Component {
|
|||||||
}}
|
}}
|
||||||
onChange={this.invChange} />
|
onChange={this.invChange} />
|
||||||
{invErrElem}
|
{invErrElem}
|
||||||
<select
|
|
||||||
value={this.state.securityValue}
|
|
||||||
onChange={this.securityChange}>
|
|
||||||
<option value="village">Village</option>
|
|
||||||
<option value="channel">Channel</option>
|
|
||||||
<option value="journal">Journal</option>
|
|
||||||
<option value="mailbox">Mailbox</option>
|
|
||||||
</select>
|
|
||||||
<p className="body-medium mt3 db">Chat History</p>
|
|
||||||
<div className="db mt2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={this.state.allowHistory}
|
|
||||||
onChange={this.allowHistoryChange.bind(this)}
|
|
||||||
className="dib mr2"
|
|
||||||
/>
|
|
||||||
<p className="body-small db mt2 mb3 dib">
|
|
||||||
Allow participants to download the chat history upon joining.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
onClick={this.onClickCreate.bind(this)}
|
onClick={this.onClickCreate.bind(this)}
|
||||||
className={createClasses}
|
className={createClasses}
|
||||||
style={{ fontSize: '18px' }}
|
>Start Chat</button>
|
||||||
>-> Create</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -65,128 +65,176 @@ export class Root extends Component {
|
|||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div>
|
<div>
|
||||||
<Route exact path="/~chat"
|
<Route
|
||||||
render={ (props) => {
|
exact
|
||||||
return (
|
path="/~chat"
|
||||||
<Skeleton sidebar={renderChannelSidebar(props)}>
|
render={props => {
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
return (
|
||||||
<div className="pl3 pr3 pt2 pb3">
|
<Skeleton
|
||||||
<h2>Home</h2>
|
chatHideonMobile={true}
|
||||||
<p className="body-regular-400 pt3">
|
sidebarShown={state.sidebarShown}
|
||||||
<Link to="/~chat/new">Create a new chat</Link> or
|
sidebar={renderChannelSidebar(props)}
|
||||||
<Link to="/~chat/join">join an existing one.</Link>
|
>
|
||||||
</p>
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-gray0">
|
||||||
|
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||||
|
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||||
|
Select, create, or join a chat to begin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Skeleton>
|
||||||
</Skeleton>
|
);
|
||||||
);
|
}}
|
||||||
}} />
|
/>
|
||||||
<Route exact path="/~chat/new"
|
<Route
|
||||||
render={ (props) => {
|
exact
|
||||||
return (
|
path="/~chat/new"
|
||||||
<Skeleton
|
render={props => {
|
||||||
spinner={this.state.spinner}
|
return (
|
||||||
sidebar={renderChannelSidebar(props)}>
|
<Skeleton
|
||||||
<NewScreen
|
sidebarHideOnMobile={true}
|
||||||
setSpinner={this.setSpinner}
|
spinner={this.state.spinner}
|
||||||
api={api}
|
sidebar={renderChannelSidebar(props)}
|
||||||
inbox={state.inbox || {}}
|
sidebarShown={state.sidebarShown}
|
||||||
{...props}
|
>
|
||||||
/>
|
<NewScreen
|
||||||
</Skeleton>
|
setSpinner={this.setSpinner}
|
||||||
);
|
api={api}
|
||||||
}} />
|
inbox={state.inbox || {}}
|
||||||
<Route exact path="/~chat/join"
|
{...props}
|
||||||
render={ (props) => {
|
/>
|
||||||
return (
|
</Skeleton>
|
||||||
<Skeleton sidebar={renderChannelSidebar(props)}>
|
);
|
||||||
<JoinScreen
|
}}
|
||||||
api={api}
|
/>
|
||||||
inbox={state.inbox}
|
<Route
|
||||||
{...props}
|
exact
|
||||||
/>
|
path="/~chat/join/:ship?/:station?"
|
||||||
</Skeleton>
|
render={props => {
|
||||||
);
|
let station =
|
||||||
}} />
|
props.match.params.ship
|
||||||
<Route exact path="/~chat/room/:ship/:station+"
|
+ "/" +
|
||||||
render={ (props) => {
|
props.match.params.station;
|
||||||
let station =
|
return (
|
||||||
`/${props.match.params.ship}/${props.match.params.station}`;
|
<Skeleton
|
||||||
let mailbox = state.inbox[station] || {
|
sidebarHideOnMobile={true}
|
||||||
config: {
|
sidebar={renderChannelSidebar(props)}
|
||||||
read: 0,
|
sidebarShown={state.sidebarShown}
|
||||||
length: 0
|
>
|
||||||
},
|
<JoinScreen api={api} inbox={state.inbox} autoJoin={station} {...props} />
|
||||||
envelopes: []
|
</Skeleton>
|
||||||
};
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/~chat/(popout)?/room/:ship/:station+"
|
||||||
|
render={props => {
|
||||||
|
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
||||||
|
let mailbox = state.inbox[station] || {
|
||||||
|
config: {
|
||||||
|
read: -1,
|
||||||
|
length: 0
|
||||||
|
},
|
||||||
|
envelopes: []
|
||||||
|
};
|
||||||
|
|
||||||
let write = state.groups[`/chat${station}/write`] || new Set([]);
|
let write = state.groups[`/chat${station}/write`] || new Set([]);
|
||||||
|
|
||||||
return (
|
let popout = props.match.url.includes("/popout/");
|
||||||
<Skeleton sidebar={renderChannelSidebar(props) }>
|
|
||||||
<ChatScreen
|
|
||||||
api={api}
|
|
||||||
subscription={subscription}
|
|
||||||
read={mailbox.config.read}
|
|
||||||
length={mailbox.config.length}
|
|
||||||
envelopes={mailbox.envelopes}
|
|
||||||
inbox={state.inbox}
|
|
||||||
group={write}
|
|
||||||
permissions={state.permissions}
|
|
||||||
pendingMessages={state.pendingMessages}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Skeleton>
|
|
||||||
);
|
|
||||||
}} />
|
|
||||||
<Route exact path="/~chat/members/:ship/:station+"
|
|
||||||
render={ (props) => {
|
|
||||||
let station =
|
|
||||||
`/${props.match.params.ship}/${props.match.params.station}`;
|
|
||||||
let read = state.permissions[`/chat${station}/read`] || {
|
|
||||||
kind: '',
|
|
||||||
who: new Set([])
|
|
||||||
};
|
|
||||||
let write = state.permissions[`/chat${station}/write`] || {
|
|
||||||
kind: '',
|
|
||||||
who: new Set([])
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton sidebar={renderChannelSidebar(props) }>
|
<Skeleton
|
||||||
<MemberScreen
|
sidebarHideOnMobile={true}
|
||||||
{...props}
|
popout={popout}
|
||||||
api={api}
|
sidebarShown={state.sidebarShown}
|
||||||
read={read}
|
sidebar={renderChannelSidebar(props)}
|
||||||
write={write}
|
>
|
||||||
permissions={state.permissions}
|
<ChatScreen
|
||||||
/>
|
api={api}
|
||||||
</Skeleton>
|
subscription={subscription}
|
||||||
);
|
read={mailbox.config.read}
|
||||||
}} />
|
envelopes={mailbox.envelopes}
|
||||||
<Route exact path="/~chat/settings/:ship/:station+"
|
inbox={state.inbox}
|
||||||
render={ (props) => {
|
group={write}
|
||||||
let station =
|
permissions={state.permissions}
|
||||||
`/${props.match.params.ship}/${props.match.params.station}`;
|
pendingMessages={state.pendingMessages}
|
||||||
let write = state.groups[`/chat${station}/write`] || new Set([]);
|
popout={popout}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/~chat/(popout)?/members/:ship/:station+"
|
||||||
|
render={props => {
|
||||||
|
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
||||||
|
let read = state.permissions[`/chat${station}/read`] || {
|
||||||
|
kind: "",
|
||||||
|
who: new Set([])
|
||||||
|
};
|
||||||
|
let write = state.permissions[`/chat${station}/write`] || {
|
||||||
|
kind: "",
|
||||||
|
who: new Set([])
|
||||||
|
};
|
||||||
|
let popout = props.match.url.includes("/popout/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
spinner={this.state.spinner}
|
sidebarHideOnMobile={true}
|
||||||
sidebar={renderChannelSidebar(props) }>
|
sidebarShown={state.sidebarShown}
|
||||||
<SettingsScreen
|
popout={popout}
|
||||||
{...props}
|
sidebar={renderChannelSidebar(props)}
|
||||||
setSpinner={this.setSpinner}
|
>
|
||||||
api={api}
|
<MemberScreen
|
||||||
group={write}
|
{...props}
|
||||||
inbox={state.inbox}
|
api={api}
|
||||||
/>
|
read={read}
|
||||||
</Skeleton>
|
write={write}
|
||||||
);
|
permissions={state.permissions}
|
||||||
}} />
|
popout={popout}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/~chat/(popout)?/settings/:ship/:station+"
|
||||||
|
render={props => {
|
||||||
|
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
||||||
|
let write = state.groups[`/chat${station}/write`] || new Set([]);
|
||||||
|
|
||||||
|
let popout = props.match.url.includes("/popout/");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
sidebarHideOnMobile={true}
|
||||||
|
spinner={this.state.spinner}
|
||||||
|
popout={popout}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
sidebar={renderChannelSidebar(props)}
|
||||||
|
>
|
||||||
|
<SettingsScreen
|
||||||
|
{...props}
|
||||||
|
setSpinner={this.setSpinner}
|
||||||
|
api={api}
|
||||||
|
group={write}
|
||||||
|
inbox={state.inbox}
|
||||||
|
popout={popout}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { deSig } from '/lib/util';
|
import { deSig } from '/lib/util';
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
import { store } from "/store";
|
||||||
|
|
||||||
|
|
||||||
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
import { ChatTabBar } from '/components/lib/chat-tabbar';
|
||||||
|
|
||||||
@ -43,22 +46,25 @@ export class SettingsScreen extends Component {
|
|||||||
renderDelete() {
|
renderDelete() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
let titleText = "Delete Chat";
|
let chatOwner = (deSig(props.match.params.ship) === window.ship);
|
||||||
let descriptionText = "Permanently delete this chat.";
|
|
||||||
let buttonText = "-> Delete";
|
|
||||||
|
|
||||||
if (deSig(props.match.params.ship) !== window.ship) {
|
let deleteButtonClasses = (chatOwner) ? 'b--red2 red2 pointer' : 'b--grey3 grey3 c-default';
|
||||||
titleText = "Leave Chat"
|
let leaveButtonClasses = (!chatOwner) ? "pointer" : "c-default";
|
||||||
descriptionText = "You will no longer have access to this chat."
|
|
||||||
buttonText = "-> Leave";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-50 fl pl2 mt3">
|
<div>
|
||||||
<p className="body-regular">{titleText}</p>
|
<div className={"w-100 fl mt3 " + ((chatOwner) ? 'o-30' : '')}>
|
||||||
<p className="label-regular gray mb3">{descriptionText}</p>
|
<p className="f8 mt3 lh-copy db">Leave Chat</p>
|
||||||
<a onClick={this.deleteChat.bind(this)}
|
<p className="f9 gray2 db mb4">Remove this chat from your chat list. You will need to request for access again.</p>
|
||||||
className="pointer btn-font underline nice-red">{buttonText}</a>
|
<a onClick={(!chatOwner) ? this.deleteChat.bind(this) : null}
|
||||||
|
className={"dib f9 black ba pa2 b--black " + 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">Permenantly 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -76,35 +82,112 @@ export class SettingsScreen extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
||||||
<div className='pl3 pt2 bb mb3'>
|
<div
|
||||||
<h2>{state.station.substr(1)}</h2>
|
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="pl3 pt4 bb b--gray4 flex relative overflow-x-scroll flex-shrink-0"
|
||||||
|
style={{ height: 48 }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="pointer"
|
||||||
|
onClick={() => {
|
||||||
|
store.setState(previousState => ({
|
||||||
|
sidebarShown: !previousState.sidebarShown
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={`v-btm pr3 dn ` + popoutSwitcher}
|
||||||
|
src={
|
||||||
|
this.props.sidebarShown
|
||||||
|
? "/~chat/img/ChatSwitcherLink.png"
|
||||||
|
: "/~chat/img/ChatSwitcherClosed.png"
|
||||||
|
}
|
||||||
|
height="16"
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<Link to={`/~chat/` + isinPopout + `room` + state.station}>
|
||||||
|
<h2
|
||||||
|
className="mono dib f7 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}
|
||||||
|
>
|
||||||
|
{state.station.substr(1)}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
<ChatTabBar
|
<ChatTabBar
|
||||||
{...props}
|
{...props}
|
||||||
station={state.station}
|
station={state.station}
|
||||||
numPeers={writeGroup.length} />
|
numPeers={writeGroup.length} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 cf pa3">
|
<div className="w-100 pl3 mt4 cf">
|
||||||
<h2>{text}</h2>
|
<h2 className="f8 pb2">{text}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let popoutSwitcher = this.props.popout
|
||||||
|
? "dn-m dn-l dn-xl"
|
||||||
|
: "dib-m dib-l dib-xl";
|
||||||
|
let isinPopout = this.props.popout ? "popout/" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
||||||
<div className='pl3 pt2 bb mb3'>
|
<div
|
||||||
<h2>{state.station.substr(1)}</h2>
|
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="pl3 pt4 bb b--gray4 flex relative overflow-x-scroll flex-shrink-0"
|
||||||
|
style={{ height: 48 }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="pointer"
|
||||||
|
onClick={() => {
|
||||||
|
store.setState(previousState => ({
|
||||||
|
sidebarShown: !previousState.sidebarShown
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={`v-btm pr3 dn ` + popoutSwitcher}
|
||||||
|
src={
|
||||||
|
this.props.sidebarShown
|
||||||
|
? "/~chat/img/ChatSwitcherLink.png"
|
||||||
|
: "/~chat/img/ChatSwitcherClosed.png"
|
||||||
|
}
|
||||||
|
height="16"
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<Link to={`/~chat/` + isinPopout + `room` + state.station}>
|
||||||
|
<h2
|
||||||
|
className="mono dib f7 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}
|
||||||
|
>
|
||||||
|
{state.station.substr(1)}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
<ChatTabBar
|
<ChatTabBar
|
||||||
{...props}
|
{...props}
|
||||||
station={state.station}
|
station={state.station}
|
||||||
numPeers={writeGroup.length}
|
numPeers={writeGroup.length}
|
||||||
isOwner={deSig(props.match.params.ship) === window.ship} />
|
isOwner={deSig(props.match.params.ship) === window.ship}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 cf pa3">
|
<div className="w-100 pl3 mt4 cf">
|
||||||
<h2>Settings</h2>
|
<h2 className="f8 pb2">Chat Settings</h2>
|
||||||
{this.renderDelete()}
|
{this.renderDelete()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ export class Sidebar extends Component {
|
|||||||
this.props.history.push('/~chat/new');
|
this.props.history.push('/~chat/new');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickJoin() {
|
||||||
|
this.props.history.push('/~chat/join')
|
||||||
|
}
|
||||||
|
|
||||||
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}`;
|
||||||
@ -67,20 +71,23 @@ export class Sidebar extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
|
<div className="h-100-minus-96-s h-100 w-100 overflow-x-hidden flex flex-column relative z1">
|
||||||
<div className="pl3 pr3 pt2 pb3 cf bb b--black-30" style={{height: '88px'}}>
|
<div className="overflow-y-auto h-100">
|
||||||
<h2 className="dib w-50 gray"><Link to="/~chat">Chat</Link></h2>
|
|
||||||
<a
|
|
||||||
className="dib tr w-50 pointer plus-font"
|
|
||||||
onClick={this.onClickNew.bind(this)}>+</a>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-y-auto" style={{
|
|
||||||
height: 'calc(100vh - 60px - 48px)'
|
|
||||||
}}>
|
|
||||||
{sidebarInvites}
|
{sidebarInvites}
|
||||||
{sidebarItems}
|
{sidebarItems}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute z2 tc w-100 bg-transparent"
|
||||||
|
style={{ bottom: 10 }}>
|
||||||
|
<a className="dib f9 pa3 bt bb bl br tc pointer bg-white"
|
||||||
|
onClick={this.onClickNew.bind(this)}>
|
||||||
|
Create New Chat
|
||||||
|
</a>
|
||||||
|
<a className="dib f9 pa3 bt bb br tl pointer bg-white"
|
||||||
|
onClick={this.onClickJoin.bind(this)}>
|
||||||
|
Join Existing Chat
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,53 @@ import { HeaderBar } from '/components/lib/header-bar.js';
|
|||||||
|
|
||||||
export class Skeleton extends Component {
|
export class Skeleton extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
|
let sidebarHide = (!this.props.sidebarShown || this.props.popout)
|
||||||
|
? "dn"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
let sidebarHideOnMobile = this.props.sidebarHideOnMobile
|
||||||
|
? "dn-s"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
let chatHideOnMobile = this.props.chatHideonMobile
|
||||||
|
? "dn-s"
|
||||||
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-100 w-100 absolute">
|
<div className="h-100 w-100 absolute">
|
||||||
<HeaderBar spinner={this.props.spinner}/>
|
<HeaderBar spinner={this.props.spinner} popout={this.props.popout} />
|
||||||
<div className="cf w-100 absolute flex"
|
<div className={`cf w-100 absolute flex ` +
|
||||||
style={{
|
((this.props.chatHideonMobile)
|
||||||
height: 'calc(100% - 48px)'
|
? "h-100 "
|
||||||
}}>
|
: "h-100-minus-48-s ") +
|
||||||
<div className="fl h-100 br b--black-30 overflow-x-hidden" style={{ flexBasis: 320 }}>
|
((this.props.popout)
|
||||||
|
? "h-100"
|
||||||
|
: "h-minus-48-m h-100-minus-48-l h-100-minus-48-xl")}>
|
||||||
|
<div className={
|
||||||
|
`fl h-100 br b--gray4 overflow-x-hidden
|
||||||
|
flex-basis-full-s flex-basis-300-m flex-basis-300-l
|
||||||
|
flex-basis-300-xl ` +
|
||||||
|
sidebarHide + " " +
|
||||||
|
sidebarHideOnMobile
|
||||||
|
}>
|
||||||
|
<div className={
|
||||||
|
chatHideOnMobile === ""
|
||||||
|
? "dn"
|
||||||
|
: "db dn-m dn-l dn-xl w-100 inter pt4 f8"
|
||||||
|
}>
|
||||||
|
<a className="pl3 pb6" href="/">
|
||||||
|
{"⟵ Landscape"}
|
||||||
|
</a>
|
||||||
|
<div className="bb b--gray4 inter f8 pl3 pt6 pb3">All Chats</div>
|
||||||
|
</div>
|
||||||
{this.props.sidebar}
|
{this.props.sidebar}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-100 fr" style={{
|
<div className={"h-100 fr " + chatHideOnMobile}
|
||||||
flexGrow: 1,
|
style={{
|
||||||
width: 'calc(100% - 320px)'
|
flexGrow: 1,
|
||||||
}}>
|
width: "calc(100% - 300px)"
|
||||||
|
}}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ class Store {
|
|||||||
permissions: {},
|
permissions: {},
|
||||||
invites: {},
|
invites: {},
|
||||||
spinner: false,
|
spinner: false,
|
||||||
|
sidebarShown: true,
|
||||||
pendingMessages: new Map([])
|
pendingMessages: new Map([])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user