mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-03 02:35:52 +03:00
link fe: metadata integration
- finds resources & displays details using metadata-store - supports creating new collections for groups - supports creating new collections with new unmanaged group - supports receiving invites for new (unmanaged) collections
This commit is contained in:
parent
115fd1c14c
commit
0539f3ca1f
@ -10,7 +10,10 @@
|
||||
:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission
|
||||
:: /json/seen mark-as-read updates
|
||||
::
|
||||
/+ *link, *server, default-agent, verb
|
||||
/- *link-view,
|
||||
metadata-store, *invite-store, group-store,
|
||||
group-hook, permission-hook, metadata-hook
|
||||
/+ *link, *server, default-agent, verb, dbug
|
||||
::
|
||||
|%
|
||||
+$ state-0
|
||||
@ -25,6 +28,7 @@
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
@ -42,6 +46,12 @@
|
||||
::
|
||||
=+ [dap.bowl /tile '/~link/js/tile.js']
|
||||
[%pass /launch %agent [our.bowl %launch] %poke %launch-action !>(-)]
|
||||
::
|
||||
=+ [%invite-action !>([%create /link])]
|
||||
[%pass /invitatory/create %agent [our.bowl %invite-store] %poke -]
|
||||
::
|
||||
=+ /invitatory/link
|
||||
[%pass - %agent [our.bowl %invite-store] %watch -]
|
||||
==
|
||||
::
|
||||
++ on-save !>(state)
|
||||
@ -65,6 +75,9 @@
|
||||
::
|
||||
%link-action
|
||||
[(handle-action:do !<(action vase)) ~]
|
||||
::
|
||||
%link-view-action
|
||||
(handle-view-action:do !<(view-action vase))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
@ -104,13 +117,18 @@
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
%kick
|
||||
:_ this
|
||||
[%pass wire %agent [our.bowl %link-store] %watch wire]~
|
||||
=/ app=term
|
||||
?: ?=([%invites *] wire)
|
||||
%invite-store
|
||||
%link-store
|
||||
[%pass wire %agent [our.bowl app] %watch wire]~
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark (on-agent:def wire sign)
|
||||
%link-initial [~ this]
|
||||
%invite-update [(handle-invite-update:do !<(invite-update vase)) this]
|
||||
%link-initial [~ this]
|
||||
::
|
||||
%link-update
|
||||
:_ this
|
||||
@ -217,10 +235,116 @@
|
||||
%- as-octs:mimes:html
|
||||
.^(@ %cx path)
|
||||
::
|
||||
++ handle-invite-update
|
||||
|= upd=invite-update
|
||||
^- (list card)
|
||||
?. ?=(%accepted -.upd) ~
|
||||
?. =(/link path.upd) ~
|
||||
|^
|
||||
:~ :: sync the group
|
||||
::
|
||||
%^ do-poke %group-hook
|
||||
%group-hook-action
|
||||
!> ^- group-hook-action:group-hook
|
||||
[%add ship path]:invite.upd
|
||||
::
|
||||
:: sync the metadata
|
||||
::
|
||||
%^ do-poke %metadata-hook
|
||||
%metadata-hook-action
|
||||
!> ^- metadata-hook-action:metadata-hook
|
||||
[%add-synced ship path]:invite.upd
|
||||
==
|
||||
::
|
||||
++ do-poke
|
||||
|= [app=term =mark =vase]
|
||||
^- card
|
||||
[%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase]
|
||||
--
|
||||
::
|
||||
++ handle-action
|
||||
|= =action
|
||||
^- card
|
||||
[%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)]
|
||||
::
|
||||
++ handle-view-action
|
||||
|= act=view-action
|
||||
^- (list card)
|
||||
?> ?=(%create -.act)
|
||||
=/ group-path=path
|
||||
?- -.members.act
|
||||
%group path.members.act
|
||||
%ships [~.~ (scot %p our.bowl) path.act]
|
||||
==
|
||||
|^
|
||||
=; group-setup=(list card)
|
||||
%+ weld group-setup
|
||||
:~ :: add collection to metadata-store
|
||||
::
|
||||
%^ do-poke %metadata-store
|
||||
%metadata-action
|
||||
!> ^- metadata-action:metadata-store
|
||||
:^ %add group-path
|
||||
[%link path.act]
|
||||
%* . *metadata:metadata-store
|
||||
title title.act
|
||||
description description.act
|
||||
date-created now.bowl
|
||||
creator our.bowl
|
||||
==
|
||||
==
|
||||
?: ?=(%group -.members.act) ~
|
||||
:* :: create the new group
|
||||
::
|
||||
%^ do-poke %group-store
|
||||
%group-action
|
||||
!> ^- group-action:group-store
|
||||
[%bundle group-path]
|
||||
::
|
||||
:: fill the new group
|
||||
::
|
||||
%^ do-poke %group-store
|
||||
%group-action
|
||||
!> ^- group-action:group-store
|
||||
[%add (~(put in ships.members.act) our.bowl) group-path]
|
||||
::
|
||||
:: make group available
|
||||
::
|
||||
%^ do-poke %group-hook
|
||||
%group-hook-action
|
||||
!> ^- group-hook-action:group-hook
|
||||
[%add our.bowl group-path]
|
||||
::
|
||||
:: make a permission equivalent
|
||||
::
|
||||
%^ do-poke %permission-hook
|
||||
%permission-hook-action
|
||||
!> ^- permission-hook-action:permission-hook
|
||||
[%add-owned group-path group-path]
|
||||
::
|
||||
:: send invites
|
||||
::
|
||||
%+ turn ~(tap in ships.members.act)
|
||||
|= =ship
|
||||
^- card
|
||||
%^ do-poke %invite-hook
|
||||
%invite-action
|
||||
!> ^- invite-action
|
||||
:^ %invite /link
|
||||
(sham group-path eny.bowl)
|
||||
:* our.bowl
|
||||
%group-hook
|
||||
group-path
|
||||
ship
|
||||
title.act
|
||||
==
|
||||
==
|
||||
::
|
||||
++ do-poke
|
||||
|= [app=term =mark =vase]
|
||||
^- card
|
||||
[%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase]
|
||||
--
|
||||
:: +give-tile-data: total unread count as json object
|
||||
::
|
||||
::NOTE the full recalc of totals here probably isn't the end of the world.
|
||||
|
24
pkg/arvo/mar/link/view-action.hoon
Normal file
24
pkg/arvo/mar/link/view-action.hoon
Normal file
@ -0,0 +1,24 @@
|
||||
/- *link-view
|
||||
=, dejs:format
|
||||
|_ act=view-action
|
||||
++ grab
|
||||
|%
|
||||
++ noun view-action
|
||||
++ json
|
||||
|^ %- of
|
||||
:~ %create^(ot 'path'^pa 'title'^so 'description'^so 'members'^mems ~)
|
||||
==
|
||||
::
|
||||
++ mems
|
||||
%- of
|
||||
:~ %group^pa
|
||||
%ships^(cu sy (ar (su ;~(pfix sig fed:ag))))
|
||||
==
|
||||
--
|
||||
--
|
||||
::
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
--
|
||||
--
|
12
pkg/arvo/sur/link-view.hoon
Normal file
12
pkg/arvo/sur/link-view.hoon
Normal file
@ -0,0 +1,12 @@
|
||||
:: link-view: encapsulating link management
|
||||
::
|
||||
|%
|
||||
++ view-action
|
||||
$% $: %create
|
||||
=path
|
||||
title=@t
|
||||
description=@t
|
||||
members=$%([%group =path] [%ships ships=(set ship)])
|
||||
==
|
||||
==
|
||||
--
|
@ -86,7 +86,7 @@ class UrbitApi {
|
||||
inviteAccept(uid) {
|
||||
this.inviteAction({
|
||||
accept: {
|
||||
path: '/chat',
|
||||
path: '/link',
|
||||
uid
|
||||
}
|
||||
});
|
||||
@ -95,7 +95,7 @@ class UrbitApi {
|
||||
inviteDecline(uid) {
|
||||
this.inviteAction({
|
||||
decline: {
|
||||
path: '/chat',
|
||||
path: '/link',
|
||||
uid
|
||||
}
|
||||
});
|
||||
@ -144,6 +144,13 @@ class UrbitApi {
|
||||
);
|
||||
}
|
||||
|
||||
createCollection(path, title, description, members) {
|
||||
// members is either {group:'/group-path'} or {'ships':[~zod]}
|
||||
return this.action("link-view", "link-view-action", {
|
||||
create: {path, title, description, members}
|
||||
})
|
||||
}
|
||||
|
||||
linkAction(data) {
|
||||
return this.action("link-store", "link-action", data);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { ChannelsItem } from '/components/lib/channels-item';
|
||||
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
||||
|
||||
export class ChannelsSidebar extends Component {
|
||||
// drawer to the left
|
||||
@ -9,42 +10,21 @@ export class ChannelsSidebar extends Component {
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let privateChannel =
|
||||
Object.keys(props.groups)
|
||||
.filter((path) => {
|
||||
return (path === "/~/default")
|
||||
})
|
||||
.map((path) => {
|
||||
let name = "Private"
|
||||
let selected = (props.selected === path);
|
||||
let linkCount = !!props.links[path] ? props.links[path].totalItems : 0;
|
||||
const unseenCount = !!props.links[path]
|
||||
? props.links[path].unseenCount
|
||||
: linkCount
|
||||
let sidebarInvites = Object.keys(props.invites)
|
||||
.map((uid) => {
|
||||
return (
|
||||
<ChannelsItem
|
||||
key={path}
|
||||
link={path}
|
||||
memberList={props.groups[path]}
|
||||
selected={selected}
|
||||
linkCount={linkCount}
|
||||
unseenCount={unseenCount}
|
||||
name={name}/>
|
||||
)
|
||||
})
|
||||
<SidebarInvite
|
||||
uid={uid}
|
||||
invite={props.invites[uid]}
|
||||
api={props.api} />
|
||||
);
|
||||
});
|
||||
|
||||
let channelItems =
|
||||
Object.keys(props.groups)
|
||||
.filter((path) => {
|
||||
return (!path.startsWith("/~/"))
|
||||
})
|
||||
.map((path) => {
|
||||
let name = path.substr(1);
|
||||
let nameSeparator = name.indexOf("/");
|
||||
name = name.substr(nameSeparator + 1);
|
||||
|
||||
let selected = (props.selected === path);
|
||||
let linkCount = !!props.links[path] ? props.links[path].totalItems : 0;
|
||||
const channelItems =
|
||||
Object.keys(props.resources).map((path) => {
|
||||
const meta = props.resources[path];
|
||||
const selected = (props.selected === path);
|
||||
const linkCount = !!props.links[path] ? props.links[path].totalItems : 0;
|
||||
const unseenCount = !!props.links[path]
|
||||
? props.links[path].unseenCount
|
||||
: linkCount
|
||||
@ -53,12 +33,12 @@ export class ChannelsSidebar extends Component {
|
||||
<ChannelsItem
|
||||
key={path}
|
||||
link={path}
|
||||
memberList={props.groups[path]}
|
||||
memberList={props.groups[meta.group]}
|
||||
selected={selected}
|
||||
linkCount={linkCount}
|
||||
unseenCount={unseenCount}
|
||||
name={name}/>
|
||||
)
|
||||
name={meta.title}/>
|
||||
);
|
||||
});
|
||||
|
||||
let activeClasses = (this.props.active === "channels") ? " " : "dn-s ";
|
||||
@ -70,7 +50,7 @@ export class ChannelsSidebar extends Component {
|
||||
if (this.props.popout) {
|
||||
hiddenClasses = false;
|
||||
} else {
|
||||
hiddenClasses = this.props.sidebarShown;
|
||||
hiddenClasses = this.props.sidebarShown;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -81,15 +61,15 @@ export class ChannelsSidebar extends Component {
|
||||
: "dn")}>
|
||||
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/">⟵ Landscape</a>
|
||||
<div className="overflow-y-scroll h-100">
|
||||
<h2 className={`f8 f9-m f9-l f9-xl
|
||||
pt1 pt4-m pt4-l pt4-xl
|
||||
pr4 pb3 pb3-m pb3-l pb3-xl
|
||||
pl3 pl4-m pl4-l pl4-xl
|
||||
black-s gray2 white-d c-default
|
||||
bb b--gray4 mb2 mb0-m mb0-l mb0-xl`}>
|
||||
Your Collections
|
||||
</h2>
|
||||
{privateChannel}
|
||||
<div className="w-100 bg-transparent pa4 bb b--gray4 b--gray1-d"
|
||||
style={{paddingBottom: 13}}>
|
||||
<Link
|
||||
className="dib f9 pointer green2 gray4-d mr4"
|
||||
to={"/~link/new"}>
|
||||
New Collection
|
||||
</Link>
|
||||
</div>
|
||||
{sidebarInvites}
|
||||
{channelItems}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
|
||||
export class ChannelsItem extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let selectedClass = (props.selected)
|
||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||
let selectedClass = (props.selected)
|
||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||
: "b--gray4 b--gray2-d";
|
||||
|
||||
let memberCount = props.memberList
|
||||
@ -18,7 +18,7 @@ export class ChannelsItem extends Component {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Link to={"/~link" + props.link}>
|
||||
<Link to={"/~link/list/0" + props.link}>
|
||||
<div className={"w-100 v-mid f9 pl4 bb z1 pa3 pt4 pb4 b--gray4 b--gray1-d gray3-d pointer " + selectedClass}>
|
||||
<p className="f9 pt1">{props.name}</p>
|
||||
<p className="f9 gray2">
|
||||
|
@ -6,8 +6,8 @@ export class CommentsPagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.commentPage) - 1);
|
||||
let nextPage = "/" + (Number(props.commentPage) + 1);
|
||||
let prevPage = (Number(props.commentPage) - 1);
|
||||
let nextPage = (Number(props.commentPage) + 1);
|
||||
|
||||
let prevDisplay = ((Number(props.commentPage) > 0))
|
||||
? "dib"
|
||||
@ -26,22 +26,24 @@ export class CommentsPagination extends Component {
|
||||
className={"pb6 absolute inter f8 left-0 " + prevDisplay}
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.groupPath
|
||||
+ "/item"
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/" + prevPage
|
||||
+ "/" + encodedUrl
|
||||
+ "/comments" + prevPage}>
|
||||
+ props.resourcePath}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
<Link
|
||||
className={"pb6 absolute inter f8 right-0 " + nextDisplay}
|
||||
to={"/~link"
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.groupPath
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/item"
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/" + nextPage
|
||||
+ "/" + encodedUrl
|
||||
+ "/comments" + nextPage}>
|
||||
+ props.resourcePath}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@ export class Comments extends Component {
|
||||
) {
|
||||
this.setState({requested: this.props.commentPage});
|
||||
api.getCommentsPage(
|
||||
this.props.groupPath,
|
||||
this.props.resourcePath,
|
||||
this.props.url,
|
||||
this.props.commentPage);
|
||||
}
|
||||
@ -73,8 +73,8 @@ export class Comments extends Component {
|
||||
<div>
|
||||
{commentsList}
|
||||
<CommentsPagination
|
||||
key={props.groupPath + props.commentPage}
|
||||
groupPath={props.groupPath}
|
||||
key={props.resourcePath + props.commentPage}
|
||||
resourcePath={props.resourcePath}
|
||||
popout={props.popout}
|
||||
linkPage={props.linkPage}
|
||||
linkIndex={props.linkIndex}
|
||||
|
369
pkg/interface/link/src/js/components/lib/invite-search.js
Normal file
369
pkg/interface/link/src/js/components/lib/invite-search.js
Normal file
@ -0,0 +1,369 @@
|
||||
import React, { Component } from "react";
|
||||
import urbitOb from "urbit-ob";
|
||||
import { Sigil } from "../lib/icons/sigil";
|
||||
|
||||
export class InviteSearch extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
groups: [],
|
||||
peers: [],
|
||||
contacts: new Map,
|
||||
searchValue: "",
|
||||
searchResults: {
|
||||
groups: [],
|
||||
ships: []
|
||||
},
|
||||
inviteError: false
|
||||
};
|
||||
this.search = this.search.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.peerUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps !== this.props) {
|
||||
this.peerUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
peerUpdate() {
|
||||
let groups = Array.from(Object.keys(this.props.groups));
|
||||
groups = groups.filter(e => !e.startsWith("/~/"));
|
||||
|
||||
let peers = [],
|
||||
peerSet = new Set(),
|
||||
contacts = new Map;
|
||||
Object.keys(this.props.groups).map(group => {
|
||||
if (this.props.groups[group].size > 0) {
|
||||
let groupEntries = this.props.groups[group].values();
|
||||
for (let member of groupEntries) {
|
||||
peerSet.add(member);
|
||||
}
|
||||
}
|
||||
if (this.props.contacts[group]) {
|
||||
let groupEntries = this.props.groups[group].values();
|
||||
for (let member of groupEntries) {
|
||||
if (this.props.contacts[group][member]) {
|
||||
if (contacts.has(member)) {
|
||||
contacts.get(member).push(this.props.contacts[group][member].nickname);
|
||||
}
|
||||
else {
|
||||
contacts.set(member, [this.props.contacts[group][member].nickname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
peers = Array.from(peerSet);
|
||||
|
||||
this.setState({ groups: groups, peers: peers, contacts: contacts });
|
||||
}
|
||||
|
||||
search(event) {
|
||||
let searchTerm = event.target.value.toLowerCase().replace("~", "");
|
||||
|
||||
this.setState({ searchValue: event.target.value });
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
this.setState({ searchResults: { groups: [], ships: [] } });
|
||||
}
|
||||
|
||||
if (searchTerm.length > 2) {
|
||||
if (this.state.inviteError === true) {
|
||||
this.setState({ inviteError: false });
|
||||
}
|
||||
|
||||
let groupMatches = [];
|
||||
if (this.props.groupResults) {
|
||||
groupMatches = this.state.groups.filter(e => {
|
||||
return e.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
if (match.length > 0) {
|
||||
if (!(contact in shipMatches)) {
|
||||
shipMatches.push(contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteGroup() {
|
||||
let { ships } = this.props.invites;
|
||||
this.setState({
|
||||
searchValue: "",
|
||||
searchResults: { groups: [], ships: [] }
|
||||
});
|
||||
this.props.setInvite({ groups: [], ships: ships });
|
||||
}
|
||||
|
||||
deleteShip(ship) {
|
||||
let { groups, ships } = this.props.invites;
|
||||
this.setState({
|
||||
searchValue: "",
|
||||
searchResults: { groups: [], ships: [] }
|
||||
});
|
||||
ships = ships.filter(e => {
|
||||
return e !== ship;
|
||||
});
|
||||
this.props.setInvite({ groups: groups, ships: ships });
|
||||
}
|
||||
|
||||
addGroup(group) {
|
||||
this.setState({
|
||||
searchValue: "",
|
||||
searchResults: { groups: [], ships: [] }
|
||||
});
|
||||
this.props.setInvite({ groups: [group], ships: [] });
|
||||
}
|
||||
|
||||
addShip(ship) {
|
||||
let { groups, ships } = this.props.invites;
|
||||
this.setState({
|
||||
searchValue: "",
|
||||
searchResults: { groups: [], ships: [] }
|
||||
});
|
||||
if (!ships.includes(ship)) {
|
||||
ships.push(ship);
|
||||
}
|
||||
if (groups.length > 0) {
|
||||
return false;
|
||||
}
|
||||
this.props.setInvite({ groups: groups, ships: ships });
|
||||
}
|
||||
|
||||
submitShipToAdd(ship) {
|
||||
let searchTerm = ship
|
||||
.toLowerCase()
|
||||
.replace("~", "")
|
||||
.trim();
|
||||
let isValid = true;
|
||||
if (!urbitOb.isValidPatp("~" + searchTerm)) {
|
||||
isValid = false;
|
||||
}
|
||||
if (!isValid) {
|
||||
this.setState({ inviteError: true, searchValue: "" });
|
||||
} else if (isValid) {
|
||||
this.addShip(searchTerm);
|
||||
this.setState({ searchValue: "" });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let searchDisabled = false;
|
||||
if (props.invites.groups) {
|
||||
if (props.invites.groups.length > 0) {
|
||||
searchDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
let participants = <div />;
|
||||
let searchResults = <div />;
|
||||
|
||||
let invErrElem = <span />;
|
||||
if (state.inviteError) {
|
||||
invErrElem = (
|
||||
<span className="f9 inter red2 db pt2">
|
||||
Invited ships must be validly formatted ship names.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
state.searchResults.groups.length > 0 ||
|
||||
state.searchResults.ships.length > 0
|
||||
) {
|
||||
let groupHeader =
|
||||
state.searchResults.groups.length > 0 ? (
|
||||
<p className="f9 gray2 ph3">Groups</p>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
let shipHeader =
|
||||
state.searchResults.ships.length > 0 ? (
|
||||
<p className="f9 gray2 pv2 ph3">Ships</p>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
let groupResults = state.searchResults.groups.map(group => {
|
||||
return (
|
||||
<li
|
||||
key={group}
|
||||
className={
|
||||
"list mono white-d f8 pv2 ph3 pointer" +
|
||||
" hover-bg-gray4 hover-black-d"
|
||||
}
|
||||
onClick={e => this.addGroup(group)}>
|
||||
{group}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
let shipResults = state.searchResults.ships.map(ship => {
|
||||
let nicknames = (this.state.contacts.has(ship))
|
||||
? this.state.contacts.get(ship).join(", ")
|
||||
: "";
|
||||
return (
|
||||
<li
|
||||
key={ship}
|
||||
className={
|
||||
"list mono white-d f8 pv1 ph3 pointer" +
|
||||
" hover-bg-gray4 hover-black-d relative"
|
||||
}
|
||||
onClick={e => this.addShip(ship)}>
|
||||
<Sigil
|
||||
ship={"~" + ship}
|
||||
size={24}
|
||||
color="#000000"
|
||||
classes="mix-blend-diff v-mid"
|
||||
/>
|
||||
<span className="v-mid ml2 mw5 truncate dib">{"~" + ship}</span>
|
||||
<span className="absolute right-1 di truncate mw4 inter f9 pt1">{nicknames}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
searchResults = (
|
||||
<div
|
||||
className={
|
||||
"absolute bg-white bg-gray0-d white-d" +
|
||||
" pv3 z-1 w-100 mt1 ba b--white-d overflow-y-scroll mh-16"
|
||||
}>
|
||||
{groupHeader}
|
||||
{groupResults}
|
||||
{shipHeader}
|
||||
{shipResults}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let groupInvites = props.invites.groups || [];
|
||||
let shipInvites = props.invites.ships || [];
|
||||
|
||||
if (groupInvites.length > 0 || shipInvites.length > 0) {
|
||||
let groups = groupInvites.map(group => {
|
||||
return (
|
||||
<span
|
||||
key={group}
|
||||
className={
|
||||
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
|
||||
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"
|
||||
}>
|
||||
{group}
|
||||
<span
|
||||
className="white-d ml3 mono pointer"
|
||||
onClick={e => this.deleteGroup(group)}>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
let ships = shipInvites.map(ship => {
|
||||
return (
|
||||
<span
|
||||
key={ship}
|
||||
className={
|
||||
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
|
||||
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"
|
||||
}>
|
||||
{"~" + ship}
|
||||
<span
|
||||
className="white-d ml3 mono pointer"
|
||||
onClick={e => this.deleteShip(ship)}>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
participants = (
|
||||
<div
|
||||
className={
|
||||
"f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d " +
|
||||
"white-d pa3 db w-100 inter"
|
||||
}>
|
||||
<span className="db gray2">Participants</span>
|
||||
{groups} {ships}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
src="/~chat/img/search.png"
|
||||
className="absolute invert-d"
|
||||
style={{
|
||||
height: 16,
|
||||
width: 16,
|
||||
top: 14,
|
||||
left: 12
|
||||
}}
|
||||
/>
|
||||
<textarea
|
||||
ref={e => {
|
||||
this.textarea = e;
|
||||
}}
|
||||
className={
|
||||
"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"
|
||||
disabled={searchDisabled}
|
||||
rows={1}
|
||||
spellCheck={false}
|
||||
style={{
|
||||
resize: "none",
|
||||
paddingLeft: 36
|
||||
}}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
this.submitShipToAdd(this.state.searchValue);
|
||||
}
|
||||
}}
|
||||
onChange={this.search}
|
||||
value={state.searchValue}
|
||||
/>
|
||||
{searchResults}
|
||||
{participants}
|
||||
{invErrElem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InviteSearch;
|
@ -115,14 +115,13 @@ export class LinkPreview extends Component {
|
||||
</span>
|
||||
<Link
|
||||
to={
|
||||
"/~link" +
|
||||
props.groupPath +
|
||||
"/" +
|
||||
"/~link/item/" +
|
||||
props.page +
|
||||
"/" +
|
||||
props.linkIndex +
|
||||
"/" +
|
||||
base64urlEncode(props.url)
|
||||
"/0/" +
|
||||
base64urlEncode(props.url) +
|
||||
props.resourcePath
|
||||
}
|
||||
className="v-top">
|
||||
<span className="f9 inter gray2">{props.comments}</span>
|
||||
|
@ -34,7 +34,7 @@ export class LinkItem extends Component {
|
||||
}
|
||||
|
||||
markPostAsSeen() {
|
||||
api.seenLink(this.props.groupPath, this.props.url);
|
||||
api.seenLink(this.props.resourcePath, this.props.url);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -90,7 +90,7 @@ export class LinkItem extends Component {
|
||||
{this.state.timeSinceLinkPost}
|
||||
</span>
|
||||
<Link to=
|
||||
{"/~link" + props.popout + props.groupPath + "/" + props.page + "/" + props.linkIndex + "/" + encodedUrl}
|
||||
{"/~link" + props.popout + "/item/" + props.page + "/" + props.linkIndex + "/0/" + encodedUrl + props.resourcePath}
|
||||
className="v-top"
|
||||
onClick={this.markPostAsSeen}>
|
||||
<span className="f9 inter gray2">
|
||||
|
@ -20,7 +20,7 @@ export class LinkSubmit extends Component {
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
api.setSpinner(true);
|
||||
api.postLink(this.props.groupPath, link, title).then(r => {
|
||||
api.postLink(this.props.resourcePath, link, title).then(r => {
|
||||
api.setSpinner(false);
|
||||
this.setState({ linkValue: "", linkTitle: "" });
|
||||
});
|
||||
|
@ -28,14 +28,14 @@ export class LinksTabBar extends Component {
|
||||
<div className={"dib f8 pl6"}>
|
||||
<Link
|
||||
className={"no-underline " + memColor}
|
||||
to={`/~link/` + popout + `members` + props.groupPath}>
|
||||
to={`/~link/` + popout + `members` + props.resourcePath}>
|
||||
Members
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dib" style={{ width: 0 }}></div>
|
||||
)}
|
||||
<a href={`/~link/popout` + props.groupPath} target="_blank"
|
||||
<a href={`/~link/popout/list/${props.page}${props.resourcePath}`} target="_blank"
|
||||
className="dib fr">
|
||||
<img
|
||||
className={`flex-shrink-0 pr4 dn` + hidePopoutIcon}
|
||||
|
@ -5,26 +5,26 @@ export class Pagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.page) - 1);
|
||||
let nextPage = "/" + (Number(props.page) + 1);
|
||||
let prevPage = (Number(props.page) - 1);
|
||||
let nextPage = (Number(props.page) + 1);
|
||||
|
||||
let prevDisplay = ((props.currentPage > 0))
|
||||
? "dib absolute left-0"
|
||||
: "dn";
|
||||
|
||||
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
||||
? "dib absolute right-0"
|
||||
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
||||
? "dib absolute right-0"
|
||||
: "dn";
|
||||
|
||||
return (
|
||||
<div className="w-100 inter relative pv6">
|
||||
<div className={prevDisplay + " inter f8"}>
|
||||
<Link to={"/~link" + props.popout + props.groupPath + prevPage}>
|
||||
<Link to={"/~link" + props.popout + "/list/" + prevPage + props.resourcePath}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
</div>
|
||||
<div className={nextDisplay + " inter f8"}>
|
||||
<Link to={"/~link" + props.popout + props.groupPath + nextPage}>
|
||||
<Link to={"/~link" + props.popout + "/list/" + nextPage + props.resourcePath}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</div>
|
||||
|
39
pkg/interface/link/src/js/components/lib/sidebar-invite.js
Normal file
39
pkg/interface/link/src/js/components/lib/sidebar-invite.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class SidebarInvite extends Component {
|
||||
|
||||
onAccept() {
|
||||
api.invite.accept(this.props.uid);
|
||||
}
|
||||
|
||||
onDecline() {
|
||||
api.invite.decline(this.props.uid);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className='w-100 bg-transparent pa4 bb b--gray4 b--gray1-d'>
|
||||
<div className='w-100 v-mid'>
|
||||
<p className="dib f8 mono gray4-d">
|
||||
{props.invite.text}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
className="dib pointer pa2 f9 bg-green2 white mt4"
|
||||
onClick={this.onAccept.bind(this)}>
|
||||
Accept Invite
|
||||
</a>
|
||||
<a
|
||||
className="dib pointer ml4 pa2 f9 bg-black bg-gray0-d white mt4"
|
||||
onClick={this.onDecline.bind(this)}>
|
||||
Decline
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export class LinkDetail extends Component {
|
||||
// if we have no preloaded data, and we aren't expecting it, get it
|
||||
if (!this.state.data.title) {
|
||||
api.getSubmission(
|
||||
this.props.groupPath, this.props.url, this.updateData.bind(this)
|
||||
this.props.resourcePath, this.props.url, this.updateData.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -46,7 +46,7 @@ export class LinkDetail extends Component {
|
||||
api.setSpinner(true);
|
||||
|
||||
api.postComment(
|
||||
this.props.groupPath,
|
||||
this.props.resourcePath,
|
||||
url,
|
||||
this.state.comment
|
||||
).then(() => {
|
||||
@ -98,10 +98,10 @@ export class LinkDetail extends Component {
|
||||
/>
|
||||
<Link
|
||||
className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||
to={"/~link" + popout + props.groupPath + "/" + props.page}>
|
||||
to={"/~link" + popout + "/list/" + props.page + props.resourcePath}>
|
||||
{"<- Collection index"}
|
||||
</Link>
|
||||
<LinksTabBar {...props} popout={popout} groupPath={props.groupPath} />
|
||||
<LinksTabBar {...props} popout={popout} resourcePath={props.resourcePath} />
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
@ -111,7 +111,7 @@ export class LinkDetail extends Component {
|
||||
comments={comments}
|
||||
nickname={nickname}
|
||||
ship={ship}
|
||||
groupPath={props.groupPath}
|
||||
resourcePath={props.resourcePath}
|
||||
page={props.page}
|
||||
linkIndex={props.linkIndex}
|
||||
time={this.state.data.time}
|
||||
@ -143,8 +143,8 @@ export class LinkDetail extends Component {
|
||||
</button>
|
||||
</div>
|
||||
<Comments
|
||||
groupPath={props.groupPath}
|
||||
key={props.groupPath + props.commentPage}
|
||||
resourcePath={props.resourcePath}
|
||||
key={props.resourcePath + props.commentPage}
|
||||
comments={props.comments}
|
||||
commentPage={props.commentPage}
|
||||
contacts={props.contacts}
|
||||
|
@ -25,16 +25,29 @@ export class Links extends Component {
|
||||
(!this.props.links[linkPage] ||
|
||||
this.props.links.local[linkPage])
|
||||
) {
|
||||
api.getPage(this.props.groupPath, this.props.page);
|
||||
api.getPage(this.props.resourcePath, this.props.page);
|
||||
}
|
||||
}
|
||||
|
||||
markAllAsSeen() {
|
||||
api.seenLink(this.props.groupPath);
|
||||
api.seenLink(this.props.resourcePath);
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
if (!props.resource.title) {
|
||||
return (
|
||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
|
||||
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||
Loading...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
let linkPage = props.page;
|
||||
|
||||
@ -77,7 +90,7 @@ export class Links extends Component {
|
||||
color={color}
|
||||
member={member}
|
||||
comments={commentCount}
|
||||
groupPath={props.groupPath}
|
||||
resourcePath={props.resourcePath}
|
||||
popout={popout}
|
||||
/>
|
||||
)
|
||||
@ -99,26 +112,30 @@ export class Links extends Component {
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}/>
|
||||
<Link to={`/~link` + popout + props.groupPath} className="pt2">
|
||||
<Link to={`/~link${popout}/list/${props.page}${props.resourcePath}`} className="pt2">
|
||||
<h2
|
||||
className={`dib f9 fw4 v-top lh-solid` +
|
||||
(props.groupPath.includes("/~/")
|
||||
(props.resource.group.includes("/~/")
|
||||
? ""
|
||||
: " mono")}>
|
||||
{(props.groupPath.includes("/~/"))
|
||||
? "Private"
|
||||
: props.groupPath.substr(1)}
|
||||
{ props.resource.title +
|
||||
( props.resource.description
|
||||
? ": " + props.resource.description
|
||||
: ""
|
||||
)
|
||||
}
|
||||
</h2>
|
||||
</Link>
|
||||
<LinksTabBar
|
||||
{...props}
|
||||
popout={popout}
|
||||
groupPath={props.groupPath + "/" + props.page}/>
|
||||
page={props.page}
|
||||
resourcePath={props.resourcePath}/>
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="flex">
|
||||
<LinkSubmit groupPath={props.groupPath}/>
|
||||
<LinkSubmit resourcePath={props.resourcePath}/>
|
||||
</div>
|
||||
<div className="pb4">
|
||||
<span
|
||||
@ -129,9 +146,9 @@ export class Links extends Component {
|
||||
{LinkList}
|
||||
<Pagination
|
||||
{...props}
|
||||
key={props.groupPath + props.page}
|
||||
key={props.resourcePath + props.page}
|
||||
popout={popout}
|
||||
groupPath={props.groupPath}
|
||||
resourcePath={props.resourcePath}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
252
pkg/interface/link/src/js/components/new.js
Normal file
252
pkg/interface/link/src/js/components/new.js
Normal file
@ -0,0 +1,252 @@
|
||||
import React, { Component } from 'react';
|
||||
import { InviteSearch } from './lib/invite-search';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { uuid, isPatTa, deSig } from '/lib/util';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
export class NewScreen extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title: '',
|
||||
description: '',
|
||||
idName: '',
|
||||
groups: [],
|
||||
ships: [],
|
||||
idError: false,
|
||||
inviteError: false,
|
||||
createGroup: true
|
||||
};
|
||||
|
||||
this.titleChange = this.titleChange.bind(this);
|
||||
this.descriptionChange = this.descriptionChange.bind(this);
|
||||
this.setInvite = this.setInvite.bind(this);
|
||||
this.createGroupChange = this.createGroupChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (prevProps !== props) {
|
||||
let station = `/~${window.ship}/${state.idName}`;
|
||||
if (station in props.resources) {
|
||||
console.log('TODO nav', '/~chat/room' + station);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleChange(event) {
|
||||
let asciiSafe = event.target.value.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, "-");
|
||||
this.setState({
|
||||
idName: asciiSafe + '-' + Math.floor(Math.random()*10000), // uniqueness
|
||||
title: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
descriptionChange(event) {
|
||||
this.setState({
|
||||
description: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
setInvite(value) {
|
||||
this.setState({
|
||||
groups: value.groups,
|
||||
ships: value.ships
|
||||
});
|
||||
}
|
||||
|
||||
createGroupChange(event) {
|
||||
if (event.target.checked) {
|
||||
this.setState({
|
||||
createGroup: !!event.target.checked,
|
||||
security: 'village'
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
createGroup: !!event.target.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickCreate() {
|
||||
const { props, state } = this;
|
||||
|
||||
if (!state.title) {
|
||||
this.setState({
|
||||
idError: true,
|
||||
inviteError: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let appPath = `/${state.idName}`;
|
||||
|
||||
if (appPath in props.resources) {
|
||||
this.setState({
|
||||
inviteError: false,
|
||||
idError: true,
|
||||
success: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
let aud = state.ships.map(mem => `~${deSig(mem.trim())}`);
|
||||
aud.forEach((mem) => {
|
||||
if (!urbitOb.isValidPatp(mem)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({
|
||||
inviteError: true,
|
||||
idError: false,
|
||||
success: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const target = aud.length === 0
|
||||
? {group: state.groups[0]}
|
||||
: {ships: aud};
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.value = '';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: false,
|
||||
success: true,
|
||||
group: [],
|
||||
ships: []
|
||||
}, () => {
|
||||
api.setSpinner(true);
|
||||
//TODO account for state.createGroup
|
||||
let submit = api.createCollection(
|
||||
appPath,
|
||||
state.title,
|
||||
state.description,
|
||||
target
|
||||
);
|
||||
submit.then(() => {
|
||||
api.setSpinner(false);
|
||||
console.log('TODO nav', `/~link/list/0${appPath}`);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let inviteSwitchClasses = (state.security === "village")
|
||||
? "relative checked bg-green2 br3 h1 toggle v-mid z-0"
|
||||
: "relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0";
|
||||
if (state.createGroup) {
|
||||
inviteSwitchClasses = inviteSwitchClasses + " o-50";
|
||||
}
|
||||
|
||||
let createGroupClasses = state.createGroup
|
||||
? "relative checked bg-green2 br3 h1 toggle v-mid z-0"
|
||||
: "relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0";
|
||||
|
||||
let createClasses = !!state.idName
|
||||
? "pointer db f9 mt7 green2 bg-gray0-d ba pv3 ph4 b--green2"
|
||||
: "pointer db f9 mt7 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3";
|
||||
|
||||
let idClasses =
|
||||
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 " +
|
||||
"focus-b--black focus-b--white-d ";
|
||||
|
||||
let idErrElem = (<span />);
|
||||
if (state.idError) {
|
||||
idErrElem = (
|
||||
<span className="f9 inter red2 db pt2">
|
||||
Chat must have a valid name.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let createGroupToggle = <div/>
|
||||
if (state.groups.length === 0) {
|
||||
createGroupToggle = (
|
||||
<div className="mt7">
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ WebkitAppearance: "none", width: 28 }}
|
||||
className={createGroupClasses}
|
||||
onChange={this.createGroupChange}
|
||||
/>
|
||||
<span className="dib f9 white-d inter ml3">Create Group</span>
|
||||
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
|
||||
Participants will share this group across applications
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"h-100 w-100 mw6 pa3 pt4 overflow-x-hidden " +
|
||||
"bg-gray0-d white-d flex flex-column"
|
||||
}>
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||
<Link to="/~link/">{"⟵ All Collections"}</Link>
|
||||
</div>
|
||||
<h2 className="mb3 f8">New Chat</h2>
|
||||
<div className="w-100">
|
||||
<p className="f8 mt3 lh-copy db">Name</p>
|
||||
<textarea
|
||||
className={idClasses}
|
||||
placeholder="Cool Collection"
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none"
|
||||
}}
|
||||
onChange={this.titleChange}
|
||||
/>
|
||||
{idErrElem}
|
||||
<p className="f8 mt3 lh-copy db">
|
||||
Description
|
||||
<span className="gray3"> (Optional)</span>
|
||||
</p>
|
||||
<textarea
|
||||
className={idClasses}
|
||||
placeholder="The hippest links"
|
||||
rows={1}
|
||||
style={{
|
||||
resize: "none"
|
||||
}}
|
||||
onChange={this.descriptionChange}
|
||||
/>
|
||||
<p className="f8 mt4 lh-copy db">
|
||||
Invite
|
||||
<span className="gray3"> (Optional)</span>
|
||||
</p>
|
||||
<p className="f9 gray2 db mb2 pt1">
|
||||
Selected entities will be able to post to chat
|
||||
</p>
|
||||
<InviteSearch
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
groupResults={true}
|
||||
invites={{
|
||||
groups: state.groups,
|
||||
ships: state.ships
|
||||
}}
|
||||
setInvite={this.setInvite}
|
||||
/>
|
||||
{createGroupToggle}
|
||||
<button
|
||||
onClick={this.onClickCreate.bind(this)}
|
||||
className={createClasses}>
|
||||
Create Collection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { api } from '/api';
|
||||
import { subscription } from '/subscription';
|
||||
import { store } from '/store';
|
||||
import { Skeleton } from '/components/skeleton';
|
||||
import { NewScreen } from '/components/new';
|
||||
import { Links } from '/components/links-list';
|
||||
import { LinkDetail } from '/components/link';
|
||||
import { base64urlDecode } from '../lib/util';
|
||||
@ -26,10 +27,16 @@ export class Root extends Component {
|
||||
let contacts = !!state.contacts ? state.contacts : {};
|
||||
const groups = !!state.groups ? state.groups : {};
|
||||
|
||||
const resources = !!state.resources ? state.resources : {};
|
||||
let links = !!state.links ? state.links : {};
|
||||
let comments = !!state.comments ? state.comments : {};
|
||||
const seen = !!state.seen ? state.seen : {};
|
||||
|
||||
//TODO update /join/resource route in contacts
|
||||
const invites = '/link' in state.invites ?
|
||||
state.invites['/link'] : {};
|
||||
console.log('invites', invites);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Route exact path="/~link"
|
||||
@ -38,6 +45,8 @@ export class Root extends Component {
|
||||
<Skeleton
|
||||
active="channels"
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={true}
|
||||
@ -45,43 +54,63 @@ export class Root extends Component {
|
||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
|
||||
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||
Collections are shared across groups. To create a new collection, <a className="black white-d" href="/~contacts">create a group</a>.
|
||||
Select or create a collection to begin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}} />
|
||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page?"
|
||||
<Route exact path="/~link/new"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
active="channels"
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={true}
|
||||
links={links}>
|
||||
<NewScreen
|
||||
resources={resources}
|
||||
groups={groups}
|
||||
contacts={contacts}/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}/>
|
||||
<Route exact path="/~link/(popout)?/list/:page/:resource([^#]+)"
|
||||
render={ (props) => {
|
||||
// groups/contacts and link channels are the same thing in ver 1
|
||||
const resourcePath = '/' + props.match.params.resource;
|
||||
const resource = resources[resourcePath] || {};
|
||||
|
||||
let groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
let contactDetails = contacts[groupPath] || {};
|
||||
let contactDetails = contacts[resource.group] || {};
|
||||
|
||||
let page = props.match.params.page || 0;
|
||||
|
||||
let popout = props.match.url.includes("/popout/");
|
||||
|
||||
let channelLinks = !!links[groupPath]
|
||||
? links[groupPath]
|
||||
let channelLinks = !!links[resourcePath]
|
||||
? links[resourcePath]
|
||||
: {local: {}};
|
||||
|
||||
let channelComments = !!comments[groupPath]
|
||||
? comments[groupPath]
|
||||
let channelComments = !!comments[resourcePath]
|
||||
? comments[resourcePath]
|
||||
: {};
|
||||
|
||||
const channelSeen = !!seen[groupPath]
|
||||
? seen[groupPath]
|
||||
const channelSeen = !!seen[resourcePath]
|
||||
? seen[resourcePath]
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
selected={resourcePath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
@ -93,7 +122,8 @@ export class Root extends Component {
|
||||
comments={channelComments}
|
||||
seen={channelSeen}
|
||||
page={page}
|
||||
groupPath={groupPath}
|
||||
resourcePath={resourcePath}
|
||||
resource={resource}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
/>
|
||||
@ -101,36 +131,38 @@ export class Root extends Component {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page/:index/:encodedUrl/(comments)?/:commentpage?"
|
||||
<Route exact path="/~link/(popout)?/item/:page/:index/:commentpage/:encodedUrl/:resource([^#]+)"
|
||||
render={ (props) => {
|
||||
let groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
const resourcePath = '/' + props.match.params.resource;
|
||||
const resource = resources[resourcePath] || {};
|
||||
|
||||
let popout = props.match.url.includes("/popout/");
|
||||
|
||||
let contactDetails = contacts[groupPath] || {};
|
||||
let contactDetails = contacts[resource.group] || {};
|
||||
|
||||
let index = props.match.params.index || 0;
|
||||
let page = props.match.params.page || 0;
|
||||
let url = base64urlDecode(props.match.params.encodedUrl);
|
||||
|
||||
let data = !!links[groupPath]
|
||||
? !!links[groupPath][page]
|
||||
? links[groupPath][page][index]
|
||||
let data = !!links[resourcePath]
|
||||
? !!links[resourcePath][page]
|
||||
? links[resourcePath][page][index]
|
||||
: {}
|
||||
: {};
|
||||
let coms = !comments[groupPath]
|
||||
let coms = !comments[resourcePath]
|
||||
? undefined
|
||||
: comments[groupPath][url];
|
||||
: comments[resourcePath][url];
|
||||
|
||||
let commentPage = props.match.params.commentpage || 0;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
selected={resourcePath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
@ -141,7 +173,8 @@ export class Root extends Component {
|
||||
url={url}
|
||||
linkIndex={index}
|
||||
contacts={contactDetails}
|
||||
groupPath={groupPath}
|
||||
resourcePath={resourcePath}
|
||||
groupPath={resource.group}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
data={data}
|
||||
|
@ -25,6 +25,8 @@ export class Skeleton extends Component {
|
||||
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
|
||||
<ChannelsSidebar
|
||||
popout={popout}
|
||||
resources={this.props.resources}
|
||||
invites={this.props.invites}
|
||||
groups={this.props.groups}
|
||||
active={this.props.active}
|
||||
selected={this.props.selected}
|
||||
|
@ -11,6 +11,10 @@ export class InviteUpdateReducer {
|
||||
this.accepted(data, state);
|
||||
this.decline(data, state);
|
||||
}
|
||||
data = _.get(json, 'invite-initial', false);
|
||||
if (data) {
|
||||
state.invites = data;
|
||||
}
|
||||
}
|
||||
|
||||
create(json, state) {
|
||||
@ -37,7 +41,6 @@ export class InviteUpdateReducer {
|
||||
accepted(json, state) {
|
||||
let data = _.get(json, 'accepted', false);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
delete state.invites[data.path][data.uid];
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
// page size as expected from link-view.
|
||||
// must change in parallel with the +page-size in /app/link-view to
|
||||
// ensure sane behavior.
|
||||
const PAGE_SIZE = 25;
|
||||
const PAGE_SIZE = 3;
|
||||
|
||||
export class LinkUpdateReducer {
|
||||
reduce(json, state) {
|
||||
|
47
pkg/interface/link/src/js/reducers/metadata-update.js
Normal file
47
pkg/interface/link/src/js/reducers/metadata-update.js
Normal file
@ -0,0 +1,47 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class MetadataReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'metadata-update', false);
|
||||
if (data) {
|
||||
this.associations(data, state);
|
||||
this.add(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
associations(json, state) {
|
||||
let data = _.get(json, 'associations', false);
|
||||
if (data) {
|
||||
let metadata = new Map;
|
||||
Object.keys(data).map((key) => {
|
||||
let assoc = data[key];
|
||||
if (assoc['app-name'] !== 'link') {
|
||||
return;
|
||||
}
|
||||
if (state.resources[assoc['app-path']]) {
|
||||
console.error('beware! overwriting previous data', data['app-path']);
|
||||
}
|
||||
state.resources[assoc['app-path']] = {
|
||||
group: assoc['group-path'],
|
||||
...assoc.metadata
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
add(json, state) {
|
||||
let data = _.get(json, 'add', false);
|
||||
if (data) {
|
||||
if (data['app-name'] !== 'link') {
|
||||
return;
|
||||
}
|
||||
if (state.resources[data['app-path']]) {
|
||||
console.error('beware! overwriting previous data', data['app-path']);
|
||||
}
|
||||
state.resources[data['app-path']] = {
|
||||
group: data['group-path'],
|
||||
...data.metadata
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { InitialReducer } from '/reducers/initial';
|
||||
import { ContactUpdateReducer } from '/reducers/contact-update.js';
|
||||
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
||||
import { MetadataReducer } from '/reducers/metadata-update.js';
|
||||
import { InviteUpdateReducer } from '/reducers/invite-update';
|
||||
import { LinkUpdateReducer } from '/reducers/link-update';
|
||||
import { LocalReducer } from '/reducers/local.js';
|
||||
import _ from 'lodash';
|
||||
@ -11,6 +13,8 @@ class Store {
|
||||
this.state = {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
resources: {},
|
||||
invites: {},
|
||||
links: {},
|
||||
comments: {},
|
||||
seen: {},
|
||||
@ -22,6 +26,8 @@ class Store {
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.contactUpdateReducer = new ContactUpdateReducer();
|
||||
this.permissionUpdateReducer = new PermissionUpdateReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.inviteUpdateReducer = new InviteUpdateReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.linkUpdateReducer = new LinkUpdateReducer();
|
||||
this.setState = () => {};
|
||||
@ -43,6 +49,8 @@ class Store {
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.contactUpdateReducer.reduce(json, this.state);
|
||||
this.permissionUpdateReducer.reduce(json, this.state);
|
||||
this.metadataReducer.reduce(json, this.state);
|
||||
this.inviteUpdateReducer.reduce(json, this.state);
|
||||
this.localReducer.reduce(json, this.state);
|
||||
this.linkUpdateReducer.reduce(json, this.state);
|
||||
|
||||
|
@ -11,16 +11,25 @@ export class Subscription {
|
||||
}
|
||||
|
||||
initializeLinks() {
|
||||
// add invite, permissions flows once link stores are more than
|
||||
// group-specific
|
||||
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this));
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this)
|
||||
);
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this)
|
||||
);
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this));
|
||||
api.bind('/app-name/link', 'PUT', api.authTokens.ship, 'metadata-store',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this)
|
||||
);
|
||||
|
||||
// open a subscription for all submissions
|
||||
api.getPage('', 0);
|
||||
|
Loading…
Reference in New Issue
Block a user