Merge branch 'm/chat-groupify-extra' (#2546)

* origin/m/chat-groupify-extra:
  chat-view: %delete even without association
  frontend: apply ec6c2ed69 to link, publish, groups
  chat fe: clarify copy
  chat fe: support adding chat to existing group
  chat fe: invite search with/out ships
  chat-view: allow %groupify into existing group
  chat-view: add docs for %create action

Signed-off-by: Jared Tobin <jared@tlon.io>
This commit is contained in:
Jared Tobin 2020-03-19 14:35:59 +04:00
commit 7a7fe45677
No known key found for this signature in database
GPG Key ID: 0E4647D58F8A69E4
17 changed files with 315 additions and 135 deletions

View File

@ -220,20 +220,29 @@
==
::
%delete
=/ group-path (group-from-chat app-path.act)
?> ?=(^ app-path.act)
:: always just delete the chat from chat-store
::
:+ (chat-hook-poke [%remove app-path.act])
(chat-poke [%delete app-path.act])
:: if we still have metadata for the chat, remove it, and the associated
:: group if it's unmanaged
::
:: we aren't guaranteed to have metadata: the chat might have been
:: deleted by the host, which pushes metadata deletion down to us.
::
=/ group-path=(unit path)
(maybe-group-from-chat app-path.act)
?~ group-path ~
=* group u.group-path
%- zing
:~ :~ (chat-hook-poke [%remove app-path.act])
(chat-poke [%delete app-path.act])
==
:~ ?. (is-creator group %chat app-path.act) ~
[(metadata-poke [%remove group [%chat app-path.act]])]~
::
?. (is-creator group-path %chat app-path.act) ~
[(metadata-poke [%remove group-path [%chat app-path.act]])]~
::
?: (is-managed group-path) ~
:~ (group-poke [%unbundle group-path])
(metadata-hook-poke [%remove group-path])
(metadata-store-poke [%remove group-path [%chat app-path.act]])
?: (is-managed group) ~
:~ (group-poke [%unbundle group])
(metadata-hook-poke [%remove group])
(metadata-store-poke [%remove group [%chat app-path.act]])
==
==
::
@ -248,6 +257,8 @@
::
%groupify
?> ?=([%'~' ^] app-path.act)
:: retrieve old data
::
=/ data=(unit mailbox)
(scry-for (unit mailbox) %chat-store [%mailbox app-path.act])
?~ data
@ -265,26 +276,52 @@
=/ encoded-path=@ta
(scot %t (spat app-path.act))
/metadata/[encoded-path]/chat/[encoded-path]
=/ new-path=^path (slag 1 `path`app-path.act)
=/ members=(set ship)
%+ fall
(group-scry app-path.act)
*(set ship)
=/ cards-1=(list card)
(poke-chat-view-action %delete app-path.act)
=/ cards-2=(list card)
:: figure out new data
::
=/ chat-path=^path (slag 1 `path`app-path.act)
:: group-path: the group to associate with the chat
:: members: members of group, if it's new
:: new-members: new members of group, if it already exists
::
=/ [group-path=path members=(set ship) new-members=(set ship)]
?~ existing.act
[chat-path who.u.permission ~]
:+ group-path.u.existing.act
~
?. inclusive.u.existing.act ~
%- ~(dif in who.u.permission)
~| [%groupifying-with-nonexistent-group group-path.u.existing.act]
%- need
(group-scry group-path.u.existing.act)
:: make changes
::
;: weld
:: delete the old chat
::
(poke-chat-view-action %delete app-path.act)
::
:: create the new chat. if needed, creates the new group.
::
%- poke-chat-view-action
:* %create
title.metadata
description.metadata
new-path
new-path
chat-path
group-path
%village
members
&
==
%+ snoc (weld cards-1 cards-2)
(chat-poke %messages new-path envelopes.u.data)
::
:: if needed, add members to the existing group
::
?~ new-members ~
[(group-poke [%add new-members group-path])]~
::
:: import messages into the new chat
::
[(chat-poke %messages chat-path envelopes.u.data)]~
==
==
::
++ create-chat
@ -387,13 +424,13 @@
=. pax ;:(weld /=chat-store/(scot %da now.bol)/mailbox pax /noun)
.^((unit mailbox) %gx pax)
::
++ group-from-chat
++ maybe-group-from-chat
|= app-path=path
^- path
^- (unit path)
?. .^(? %gu (scot %p our.bol) %metadata-store (scot %da now.bol) ~)
?: ?=([@ ^] app-path)
~& [%assuming-ported-legacy-chat app-path]
[%'~' app-path]
`[%'~' app-path]
~& [%weird-chat app-path]
!!
=/ resource-indices
@ -404,8 +441,15 @@
(scot %da now.bol)
/resource-indices
==
=/ groups=(set path) (~(got by resource-indices) [%chat app-path])
(snag 0 ~(tap in groups))
=/ groups=(set path)
%+ fall
(~(get by resource-indices) [%chat app-path])
*(set path)
?~ groups ~
`n.groups
::
++ group-from-chat
(cork maybe-group-from-chat need)
::
++ is-managed
|= =path

View File

@ -283,7 +283,8 @@
==
::
++ groupify
(ot [%app-path pa] ~)
=- (ot [%app-path pa] [%existing -] ~)
(mu (ot [%group-path pa] [%inclusive bo] ~))
::
++ sec
=, dejs:format

View File

@ -1,7 +1,14 @@
/- *rw-security
|%
+$ chat-view-action
$% $: %create
$% :: %create: create a new chat
::
:: if :app-path and :group-path are different, :members must be empty,
:: as the :group-path is assumed to exist.
:: if :app-path and :group-path are identical, and the :group-path
:: doesn't yet exist, will create a new group with :members.
::
$: %create
title=@t
description=@t
app-path=path
@ -18,6 +25,10 @@
:: and invite the current whitelist to that group.
:: existing messages get moved over.
::
[%groupify app-path=path]
:: if :existing is provided, associates chat with that group instead
:: of creating a new one. :inclusive indicates whether or not to add
:: chat members to the group, if they aren't there already.
::
[%groupify app-path=path existing=(unit [group-path=path inclusive=?])]
==
--

View File

@ -175,8 +175,15 @@ class UrbitApi {
});
}
chatViewGroupify(path) {
return this.chatViewAction({ groupify: { 'app-path': path } });
chatViewGroupify(path, group = null, inclusive = false) {
let action = { groupify: { 'app-path': path, existing: null } };
if (group) {
action.groupify.existing = {
'group-path': group,
inclusive: inclusive
}
}
return this.chatViewAction(action);
}
inviteAction(data) {

View File

@ -65,6 +65,7 @@ export class InviteElement extends Component {
groups={{}}
contacts={props.contacts}
groupResults={false}
shipResults={true}
invites={{
groups: [],
ships: this.state.members

View File

@ -96,37 +96,37 @@ export class InviteSearch extends Component {
});
}
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
let shipMatches = [];
if (this.props.shipResults) {
shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
}
}
}
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
}
}
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
}
}
@ -203,6 +203,18 @@ export class InviteSearch extends Component {
let participants = <div />;
let searchResults = <div />;
let placeholder = '';
if (props.shipResults) {
placeholder = 'ships';
}
if (props.groupResults) {
if (placeholder.length > 0) {
placeholder = placeholder + ' or ';
}
placeholder = placeholder + 'existing groups';
}
placeholder = 'Search for ' + placeholder;
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
@ -357,7 +369,7 @@ export class InviteSearch extends Component {
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d"
}
placeholder="Search for ships or existing groups"
placeholder={placeholder}
disabled={searchDisabled}
rows={1}
spellCheck={false}

View File

@ -263,6 +263,7 @@ export class NewScreen extends Component {
contacts={props.contacts}
associations={props.associations}
groupResults={true}
shipResults={true}
invites={{
groups: state.groups,
ships: state.ships

View File

@ -280,6 +280,9 @@ export class Root extends Component {
station={station}
association={association}
permission={permission}
groups={state.groups || {}}
contacts={state.contacts || {}}
associations={associations.contacts}
api={api}
station={station}
group={group}

View File

@ -5,6 +5,7 @@ import { Route, Link } from "react-router-dom";
import { ChatTabBar } from '/components/lib/chat-tabbar';
import { InviteSearch } from '/components/lib/invite-search';
import SidebarSwitcher from './lib/icons/icon-sidebar-switch';
@ -16,10 +17,15 @@ export class SettingsScreen extends Component {
isLoading: false,
title: "",
description: "",
color: ""
color: "",
// groupify settings
targetGroup: null,
inclusive: false
};
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);
@ -58,6 +64,18 @@ export class SettingsScreen extends Component {
}
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: !!event.target.checked });
}
changeTitle() {
this.setState({title: event.target.value})
}
@ -122,7 +140,9 @@ export class SettingsScreen extends Component {
groupifyChat() {
const { props, state } = this;
props.api.chatView.groupify(props.station);
props.api.chatView.groupify(
props.station, state.targetGroup, state.inclusive
);
props.api.setSpinner(true);
this.setState({
@ -161,7 +181,6 @@ export class SettingsScreen extends Component {
const { props, state } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
console.log(chatOwner, props.match.params.ship, window.ship);
const ownedUnmanagedVillage =
chatOwner &&
@ -171,15 +190,55 @@ export class SettingsScreen extends Component {
if (!ownedUnmanagedVillage) {
return null;
} else {
let inclusiveToggle = <div/>
if (state.targetGroup) {
//TODO toggle component into /lib
let inclusiveClasses = state.inclusive
? "relative checked bg-green2 br3 h1 toggle v-mid z-0"
: "relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0";
inclusiveToggle = (
<div className="mt4">
<input
type="checkbox"
style={{ WebkitAppearance: "none", width: 28 }}
className={inclusiveClasses}
onChange={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 "}>
<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.
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 b--black b--gray1-d pointer"}>Convert to group</a>
className={"dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black b--gray1-d pointer"}>
Convert to group
</a>
</div>
</div>
);

View File

@ -73,6 +73,7 @@ export class AddScreen extends Component {
groups={props.groups}
contacts={props.contacts}
groupResults={false}
shipResults={true}
invites={this.state.invites}
setInvite={this.invChange}
/>

View File

@ -102,37 +102,37 @@ export class InviteSearch extends Component {
});
}
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
let shipMatches = [];
if (this.props.shipResults) {
shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
}
}
}
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
}
}
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
}
}
@ -209,6 +209,18 @@ export class InviteSearch extends Component {
let participants = <div />;
let searchResults = <div />;
let placeholder = '';
if (props.shipResults) {
placeholder = 'ships';
}
if (props.groupResults) {
if (placeholder.length > 0) {
placeholder = placeholder + ' or ';
}
placeholder = placeholder + 'existing groups';
}
placeholder = 'Search for ' + placeholder;
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
@ -372,7 +384,7 @@ export class InviteSearch extends Component {
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d"
}
placeholder="Search for ships or existing groups"
placeholder={placeholder}
disabled={searchDisabled}
rows={1}
spellCheck={false}

View File

@ -134,6 +134,7 @@ export class NewScreen extends Component {
groups={this.props.groups}
contacts={this.props.contacts}
groupResults={false}
shipResults={true}
invites={this.state.invites}
setInvite={this.invChange}
/>

View File

@ -57,6 +57,7 @@ export class InviteElement extends Component {
groups={{}}
contacts={props.contacts}
groupResults={false}
shipResults={true}
invites={{
groups: [],
ships: this.state.members

View File

@ -102,37 +102,37 @@ export class InviteSearch extends Component {
});
}
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
let shipMatches = [];
if (this.props.shipResults) {
shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
}
}
}
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
}
}
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
}
}
@ -209,6 +209,18 @@ export class InviteSearch extends Component {
let participants = <div />;
let searchResults = <div />;
let placeholder = '';
if (props.shipResults) {
placeholder = 'ships';
}
if (props.groupResults) {
if (placeholder.length > 0) {
placeholder = placeholder + ' or ';
}
placeholder = placeholder + 'existing groups';
}
placeholder = 'Search for ' + placeholder;
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
@ -372,7 +384,7 @@ export class InviteSearch extends Component {
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d"
}
placeholder="Search for ships or existing groups"
placeholder={placeholder}
disabled={searchDisabled}
rows={1}
spellCheck={false}

View File

@ -221,6 +221,7 @@ export class NewScreen extends Component {
groups={props.groups}
contacts={props.contacts}
groupResults={true}
shipResults={true}
invites={{
groups: state.groups,
ships: state.ships

View File

@ -102,37 +102,37 @@ export class InviteSearch extends Component {
});
}
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
let shipMatches = [];
if (this.props.shipResults) {
shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
for (let contact of this.state.contacts.keys()) {
let thisContact = this.state.contacts.get(contact);
let match = thisContact.filter(e => {
return e.toLowerCase().includes(searchTerm);
});
if (match.length > 0) {
if (!(contact in shipMatches)) {
shipMatches.push(contact);
}
}
}
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
}
}
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (shipMatches.length === 0 && isValid) {
shipMatches.push(searchTerm);
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
}
}
@ -209,6 +209,18 @@ export class InviteSearch extends Component {
let participants = <div />;
let searchResults = <div />;
let placeholder = '';
if (props.shipResults) {
placeholder = 'ships';
}
if (props.groupResults) {
if (placeholder.length > 0) {
placeholder = placeholder + ' or ';
}
placeholder = placeholder + 'existing groups';
}
placeholder = 'Search for ' + placeholder;
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
@ -372,7 +384,7 @@ export class InviteSearch extends Component {
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
" db focus-b--black focus-b--white-d"
}
placeholder="Search for ships or existing groups"
placeholder={placeholder}
disabled={searchDisabled}
rows={1}
spellCheck={false}

View File

@ -189,6 +189,7 @@ export class NewScreen extends Component {
<InviteSearch
associations={this.props.associations}
groupResults={true}
shipResults={true}
groups={this.props.groups}
contacts={this.props.contacts}
invites={this.state.invites}