mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 19:55:53 +03:00
Merge pull request #3269 from urbit/la/chat-settings
chat: settings page refactor
This commit is contained in:
commit
2b065d65dc
@ -277,44 +277,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+"
|
||||
|
@ -6,60 +6,53 @@ 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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -1,66 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export class ChatTabBar extends Component {
|
||||
render() {
|
||||
const props = this.props;
|
||||
export const ChatTabBar = (props) => {
|
||||
const {
|
||||
location,
|
||||
station
|
||||
} = props;
|
||||
let setColor = '', popout = '';
|
||||
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
51
pkg/interface/src/apps/chat/components/lib/delete-button.js
Normal file
51
pkg/interface/src/apps/chat/components/lib/delete-button.js
Normal 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>
|
||||
);
|
||||
};
|
||||
|
104
pkg/interface/src/apps/chat/components/lib/groupify-button.js
Normal file
104
pkg/interface/src/apps/chat/components/lib/groupify-button.js
Normal file
@ -0,0 +1,104 @@
|
||||
import React, { Component } from 'react';
|
||||
import Toggle from '../../../../components/toggle';
|
||||
import { InviteSearch } from '../../../../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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
71
pkg/interface/src/apps/chat/components/lib/metadata-color.js
Normal file
71
pkg/interface/src/apps/chat/components/lib/metadata-color.js
Normal file
@ -0,0 +1,71 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { uxToHex } from '../../../../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>
|
||||
);
|
||||
}
|
||||
}
|
52
pkg/interface/src/apps/chat/components/lib/metadata-input.js
Normal file
52
pkg/interface/src/apps/chat/components/lib/metadata-input.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { MetadataColor } from './metadata-color';
|
||||
import { MetadataInput } from './metadata-input';
|
||||
import { uxToHex } from '../../../../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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
import React, { Component } from 'react';
|
||||
import { deSig, uxToHex, writeText } from '../../../lib/util';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { deSig } from '../../../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 '../../../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) {
|
||||
@ -14,444 +17,127 @@ export class SettingsScreen extends Component {
|
||||
|
||||
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);
|
||||
this.changeLoading = this.changeLoading.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)}`
|
||||
});
|
||||
if (this.state.isLoading && (this.props.station in this.props.inbox)) {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { props, state } = this;
|
||||
if (Boolean(state.isLoading) && !(props.station in props.inbox)) {
|
||||
if (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)}`
|
||||
});
|
||||
} else if (state.isLoading && (props.station in props.inbox)) {
|
||||
this.setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
changeLoading(isLoading, awaiting, type, closure) {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
awaiting: true,
|
||||
type: (deSig(props.match.params.ship) === window.ship)
|
||||
? 'Deleting chat...'
|
||||
: 'Leaving chat...'
|
||||
}, (() => {
|
||||
props.api.chat.delete(props.station);
|
||||
}));
|
||||
isLoading,
|
||||
awaiting,
|
||||
type
|
||||
}, closure);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
renderLoading() {
|
||||
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>
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
|
||||
text={this.state.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderGroupify() {
|
||||
const { props, state } = this;
|
||||
renderNormal() {
|
||||
const { state } = this;
|
||||
const {
|
||||
associations,
|
||||
association,
|
||||
contacts,
|
||||
groups,
|
||||
api,
|
||||
station,
|
||||
match
|
||||
} = this.props;
|
||||
const isOwner = deSig(match.params.ship) === window.ship;
|
||||
|
||||
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>
|
||||
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 { props, state } = this;
|
||||
const isinPopout = this.props.popout ? 'popout/' : '';
|
||||
const { state } = this;
|
||||
const {
|
||||
api,
|
||||
group,
|
||||
association,
|
||||
station,
|
||||
popout,
|
||||
sidebarShown,
|
||||
match,
|
||||
location
|
||||
} = this.props;
|
||||
|
||||
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);
|
||||
}
|
||||
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">
|
||||
<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>
|
||||
<ChatHeader
|
||||
match={match}
|
||||
location={location}
|
||||
api={api}
|
||||
group={group}
|
||||
association={association}
|
||||
station={station}
|
||||
sidebarShown={sidebarShown}
|
||||
popout={popout} />
|
||||
<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} />
|
||||
{(state.isLoading) ? this.renderLoading() : this.renderNormal() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user